devcontainer :)

This commit is contained in:
Александр Кирюхин 2024-01-28 19:19:41 +00:00
parent fac2df0bc1
commit 7618d85264
27 changed files with 313 additions and 434 deletions

1
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1 @@
FROM gitrepo.ru/neonxp/devcontainer:latest

View file

@ -0,0 +1,26 @@
{
"name": "Go",
"dockerComposeFile": "./docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"forwardPorts": [5432, 5173, 8000],
"remoteUser": "vscode",
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.makefile-tools",
"redhat.vscode-yaml",
"humao.rest-client",
"mtxr.sqltools",
"mtxr.sqltools-driver-pg",
"codezombiech.gitignore",
"ms-azuretools.vscode-docker"
]
}
},
"postCreateCommand": "go mod download",
"features": {
"https://gitrepo.ru/api/packages/NeonXP/generic/features/latest/devcontainer-feature-common-utils.tgz": {},
"https://gitrepo.ru/api/packages/NeonXP/generic/features/latest/devcontainer-feature-go.tgz": {}
}
}

View file

@ -0,0 +1,24 @@
version: '3.8'
volumes:
postgres-data:
services:
app:
build:
context: .
dockerfile: Dockerfile
env_file:
- .env
volumes:
- ../..:/workspaces:cached
command: sleep infinity
network_mode: service:db
db:
image: postgres:15-alpine3.17
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
env_file:
- .env

12
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly

2
.vscode/launch.json vendored
View file

@ -10,7 +10,7 @@
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "${workspaceFolder}/", "program": "${workspaceFolder}/",
"envFile": "${workspaceFolder}/.env" "envFile": "${workspaceFolder}/.devcontainer/.env"
} }
] ]
} }

View file

@ -20,7 +20,16 @@ paths:
post: post:
security: [] security: []
requestBody: requestBody:
$ref: "#/components/requestBodies/login" content:
application/json:
schema:
type: object
properties:
email:
type: string
password:
type: string
required: [email, password]
responses: responses:
200: 200:
$ref: "#/components/responses/userResponse" $ref: "#/components/responses/userResponse"
@ -30,7 +39,20 @@ paths:
post: post:
security: [] security: []
requestBody: requestBody:
$ref: "#/components/requestBodies/register" 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: responses:
200: 200:
$ref: "#/components/responses/userResponse" $ref: "#/components/responses/userResponse"
@ -50,7 +72,6 @@ paths:
responses: responses:
200: 200:
$ref: "#/components/responses/gameListResponse" $ref: "#/components/responses/gameListResponse"
/engine/{uid}: /engine/{uid}:
get: get:
operationId: gameEngine operationId: gameEngine
@ -75,27 +96,18 @@ paths:
type: string type: string
format: uuid format: uuid
requestBody: requestBody:
$ref: "#/components/requestBodies/enterCodeRequest" content:
application/json:
schema:
type: object
properties:
code:
type: string
required:
- code
responses: responses:
200: 200:
$ref: "#/components/responses/taskResponse" $ref: "#/components/responses/taskResponse"
/file/upload:
post:
operationId: uploadFile
security:
- cookieAuth: [creator, admin]
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
responses:
200:
$ref: "#/components/responses/uploadResponse"
/file/{uid}: /file/{uid}:
get: get:
operationId: getFile operationId: getFile
@ -114,24 +126,46 @@ paths:
schema: schema:
type: string type: string
format: binary format: binary
# Admin routes
/admin/file/upload:
post:
operationId: adminUploadFile
security:
- cookieAuth: [creator, admin]
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
responses:
200:
$ref: "#/components/responses/uploadResponse"
/admin/games: /admin/games:
get: get:
operationId: listGamesByAdmin operationId: adminListGames
responses: responses:
200: 200:
$ref: "#/components/responses/gameListResponse" $ref: "#/components/responses/gameListResponse"
post: post:
operationId: createGame operationId: adminEditGame
security: security:
- cookieAuth: [creator, admin] - cookieAuth: [creator, admin]
requestBody: requestBody:
$ref: "#/components/requestBodies/gameEditRequest" content:
application/json:
schema:
$ref: "#/components/schemas/gameEdit"
responses: responses:
200: 200:
$ref: "#/components/responses/gameAdminResponse" $ref: "#/components/responses/gameAdminResponse"
/admin/games/{uid}: /admin/games/{uid}:
get: get:
operationId: getGameByAdmin operationId: adminGetGame
parameters: parameters:
- name: uid - name: uid
in: path in: path
@ -144,22 +178,7 @@ paths:
responses: responses:
200: 200:
$ref: "#/components/responses/gameAdminResponse" $ref: "#/components/responses/gameAdminResponse"
post:
operationId: editGame
parameters:
- name: uid
in: path
required: true
schema:
type: string
format: uuid
security:
- cookieAuth: [creator, admin]
requestBody:
$ref: "#/components/requestBodies/gameEditRequest"
responses:
200:
$ref: "#/components/responses/gameAdminResponse"
components: components:
schemas: schemas:
userView: userView:
@ -254,15 +273,10 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/codeView" $ref: "#/components/schemas/codeView"
# solutions:
# type: array
# items:
# $ref: '#/components/schemas/solutionView'
required: required:
- title - title
- text - text
- codes - codes
# - solutions
codeView: codeView:
type: object type: object
properties: properties:
@ -272,15 +286,6 @@ components:
type: string type: string
required: required:
- description - description
solutionView:
type: object
properties:
text:
type: string
after:
type: integer
required:
- after
gameEdit: gameEdit:
type: object type: object
properties: properties:
@ -326,15 +331,10 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/codeEdit" $ref: "#/components/schemas/codeEdit"
# solutions:
# type: array
# items:
# $ref: '#/components/schemas/solutionEdit'
required: required:
- title - title
- text - text
- codes - codes
# - solutions
codeEdit: codeEdit:
type: object type: object
properties: properties:
@ -348,78 +348,22 @@ components:
required: required:
- description - description
- code - code
# solutionEdit:
# type: object
# properties:
# text:
# type: string
# after:
# type: integer
# required:
# - after
# - text
gameType: gameType:
type: string type: string
enum: enum:
- virtual - virtual
- city - city
requestBodies:
login:
required: true
content:
"application/json":
schema:
type: object
properties:
email:
type: string
password:
type: string
required: [email, password]
register:
required: true
content:
"application/json":
schema:
type: object
properties:
username:
type: string
email:
type: string
password:
type: string
password2:
type: string
required: [username, email, password, password2]
gameEditRequest:
required: true
content:
"application/json":
schema:
$ref: "#/components/schemas/gameEdit"
enterCodeRequest:
required: true
content:
"application/json":
schema:
type: object
properties:
code:
type: string
required:
- code
responses: responses:
userResponse: userResponse:
description: "" description: ""
content: content:
"application/json": application/json:
schema: schema:
$ref: "#/components/schemas/userView" $ref: "#/components/schemas/userView"
errorResponse: errorResponse:
description: "" description: ""
content: content:
"application/json": application/json:
schema: schema:
type: object type: object
properties: properties:
@ -431,7 +375,7 @@ components:
gameListResponse: gameListResponse:
description: "" description: ""
content: content:
"application/json": application/json:
schema: schema:
type: array type: array
items: items:
@ -439,25 +383,25 @@ components:
gameResponse: gameResponse:
description: "" description: ""
content: content:
"application/json": application/json:
schema: schema:
$ref: "#/components/schemas/gameView" $ref: "#/components/schemas/gameView"
gameAdminResponse: gameAdminResponse:
description: "" description: ""
content: content:
"application/json": application/json:
schema: schema:
$ref: "#/components/schemas/gameEdit" $ref: "#/components/schemas/gameEdit"
taskResponse: taskResponse:
description: "" description: ""
content: content:
"application/json": application/json:
schema: schema:
$ref: "#/components/schemas/taskView" $ref: "#/components/schemas/taskView"
uploadResponse: uploadResponse:
description: "" description: ""
content: content:
"application/json": application/json:
schema: schema:
type: object type: object
properties: properties:

View file

@ -22,17 +22,17 @@ import (
// ServerInterface represents all server handlers. // ServerInterface represents all server handlers.
type ServerInterface interface { type ServerInterface interface {
// (POST /admin/file/upload)
AdminUploadFile(ctx echo.Context) error
// (GET /admin/games) // (GET /admin/games)
ListGamesByAdmin(ctx echo.Context) error AdminListGames(ctx echo.Context) error
// (POST /admin/games) // (POST /admin/games)
CreateGame(ctx echo.Context) error AdminEditGame(ctx echo.Context) error
// (GET /admin/games/{uid}) // (GET /admin/games/{uid})
GetGameByAdmin(ctx echo.Context, uid openapi_types.UUID) error AdminGetGame(ctx echo.Context, uid openapi_types.UUID) error
// (POST /admin/games/{uid})
EditGame(ctx echo.Context, uid openapi_types.UUID) error
// (GET /engine/{uid}) // (GET /engine/{uid})
GameEngine(ctx echo.Context, uid openapi_types.UUID) error GameEngine(ctx echo.Context, uid openapi_types.UUID) error
@ -40,9 +40,6 @@ type ServerInterface interface {
// (POST /engine/{uid}/code) // (POST /engine/{uid}/code)
EnterCode(ctx echo.Context, uid openapi_types.UUID) error EnterCode(ctx echo.Context, uid openapi_types.UUID) error
// (POST /file/upload)
UploadFile(ctx echo.Context) error
// (GET /file/{uid}) // (GET /file/{uid})
GetFile(ctx echo.Context, uid openapi_types.UUID) error GetFile(ctx echo.Context, uid openapi_types.UUID) error
@ -67,30 +64,41 @@ type ServerInterfaceWrapper struct {
Handler ServerInterface Handler ServerInterface
} }
// ListGamesByAdmin converts echo context to params. // AdminUploadFile converts echo context to params.
func (w *ServerInterfaceWrapper) ListGamesByAdmin(ctx echo.Context) error { func (w *ServerInterfaceWrapper) AdminUploadFile(ctx echo.Context) error {
var err error
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.AdminUploadFile(ctx)
return err
}
// AdminListGames converts echo context to params.
func (w *ServerInterfaceWrapper) AdminListGames(ctx echo.Context) error {
var err error var err error
ctx.Set(CookieAuthScopes, []string{}) ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments // Invoke the callback with all the unmarshaled arguments
err = w.Handler.ListGamesByAdmin(ctx) err = w.Handler.AdminListGames(ctx)
return err return err
} }
// CreateGame converts echo context to params. // AdminEditGame converts echo context to params.
func (w *ServerInterfaceWrapper) CreateGame(ctx echo.Context) error { func (w *ServerInterfaceWrapper) AdminEditGame(ctx echo.Context) error {
var err error var err error
ctx.Set(CookieAuthScopes, []string{"creator", "admin"}) ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
// Invoke the callback with all the unmarshaled arguments // Invoke the callback with all the unmarshaled arguments
err = w.Handler.CreateGame(ctx) err = w.Handler.AdminEditGame(ctx)
return err return err
} }
// GetGameByAdmin converts echo context to params. // AdminGetGame converts echo context to params.
func (w *ServerInterfaceWrapper) GetGameByAdmin(ctx echo.Context) error { func (w *ServerInterfaceWrapper) AdminGetGame(ctx echo.Context) error {
var err error var err error
// ------------- Path parameter "uid" ------------- // ------------- Path parameter "uid" -------------
var uid openapi_types.UUID var uid openapi_types.UUID
@ -103,25 +111,7 @@ func (w *ServerInterfaceWrapper) GetGameByAdmin(ctx echo.Context) error {
ctx.Set(CookieAuthScopes, []string{"creator", "admin"}) ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
// Invoke the callback with all the unmarshaled arguments // Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetGameByAdmin(ctx, uid) err = w.Handler.AdminGetGame(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 return err
} }
@ -161,17 +151,6 @@ func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error {
return err return err
} }
// UploadFile converts echo context to params.
func (w *ServerInterfaceWrapper) UploadFile(ctx echo.Context) error {
var err error
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.UploadFile(ctx)
return err
}
// GetFile converts echo context to params. // GetFile converts echo context to params.
func (w *ServerInterfaceWrapper) GetFile(ctx echo.Context) error { func (w *ServerInterfaceWrapper) GetFile(ctx echo.Context) error {
var err error var err error
@ -269,13 +248,12 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
Handler: si, Handler: si,
} }
router.GET(baseURL+"/admin/games", wrapper.ListGamesByAdmin) router.POST(baseURL+"/admin/file/upload", wrapper.AdminUploadFile)
router.POST(baseURL+"/admin/games", wrapper.CreateGame) router.GET(baseURL+"/admin/games", wrapper.AdminListGames)
router.GET(baseURL+"/admin/games/:uid", wrapper.GetGameByAdmin) router.POST(baseURL+"/admin/games", wrapper.AdminEditGame)
router.POST(baseURL+"/admin/games/:uid", wrapper.EditGame) router.GET(baseURL+"/admin/games/:uid", wrapper.AdminGetGame)
router.GET(baseURL+"/engine/:uid", wrapper.GameEngine) router.GET(baseURL+"/engine/:uid", wrapper.GameEngine)
router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode) router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode)
router.POST(baseURL+"/file/upload", wrapper.UploadFile)
router.GET(baseURL+"/file/:uid", wrapper.GetFile) router.GET(baseURL+"/file/:uid", wrapper.GetFile)
router.GET(baseURL+"/games", wrapper.GetGames) router.GET(baseURL+"/games", wrapper.GetGames)
router.GET(baseURL+"/user", wrapper.GetUser) router.GET(baseURL+"/user", wrapper.GetUser)
@ -288,27 +266,26 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
// Base64 encoded, gzipped, json marshaled Swagger object // Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{ var swaggerSpec = []string{
"H4sIAAAAAAAC/9RZzW7kNgx+lULt0RvP/px8ywZtUDQo2jTbSzAIFJuZaGNLrkRnMwj87gUl/47lseMa", "H4sIAAAAAAAC/8xY32+kNhD+Vyq3j1zY+/HEWxqlUdWoaq+5vkSryIHJxhewqT3OZRXxv1djAwuLWcge",
"2fQUR6JF8uNHmuI8s1hluZIg0bDomWn4pwCDn1UiwC6ARNBnKoFLt0NrsZII0j7yPE9FzFEoGX41StKa", "Su4pGzx4Zr75vmHsZ5aqolQSJBqWPDMNplTSgPsHtFb6c/2EHqRKIkikn7wsc5FyFErGX42S9Myk91Bw",
"ie8h4/SUa5WDxuqoWCVAf3GfA4uYQS3kjpVlYLUKDQmLrp3UNqil1O1XiJGVfTHUBZQB2/EMfk4ELrHt", "+lVqVYJG4fdJVeZex20JLGFCImxAsypiBRjDN91Fg1rIDauqiGn4zwoNGUuu/RY7+3XU2Kvbr5Aiq+iF",
"Jw13LGI/hi0Aods1YX3uiNpU7YT8D0BAxkXqQSJgOTfmm9LJNEzujM4bMyHTsBMGQb+2+e3mB+9uYUBL", "DEyqRUkxsYTR/htewGlWCHlUFr9ouGMJ+zneYRT7VRPTzueZOOj5Uhg8yrFAKMycCP4V8I281WBwrfl2",
"ns0gSCMZDEHoapkFiF0xuZKm8k1rpS+rlfW4LiTCDjQ5moExfDc3EVp5vzsJmFiLnGxiEWNVTpwmmZCL", "LCLk5mFxGGhTH0TYqS1zxbMFOGStyOjvndIFR5b4B9EEbZzRXLJYA3pxgGjTcYCqqN6m1Ynj1JSCmmz3",
"vJifFX7NF8LgIsUCITNzLPhbwDfSVoHBteb7MYuQm4fVYaBDnRF+pUWeKp6swKGiEDaT7pTOOLLILQRT", "NgysHwNad8/Iux5C6BdcZktFeyCMkP9WgQP/k6CkfmEClpnoRaxUom6fww5HAjGzNU3WvqvsazpiKDAP",
"6UFCc8lCubQ6QHToOEBlUB3T5Inl1NyvxcGBnv0loHXPDMY+QW7DeraWtUfM8OlvMnCgfxKU2G1MwDIT", "w+ofTHeLK7KrIvYojLjt7XWrVA5cDmrQWDbeoz1yuNebJFsgaoTHinZVhwvSFt6JRstz4prAbee1XYZt",
"vYDlSlQdw7DCUYKY2TlN0q6qHOZ0wFBg6ofVLUxXiyuSKwP2KIy47Z11q1QKXA5iUEvW2oMDctjXaycb", "rxvUmlu8V3o+wjs9DhFONXCE7BSPk9ors+pMWYkjywtRZY8NPs5DRGgJsAuwC2vUlusARVoNBPvK/Eq3",
"ICqEx4J2VZkLssicEo0Fp09XLHDfea31sKl1g1jzAu+Vno9wm49DhGMNHCE5xWWp9sqsOlOFxJHtlahy", "3TRQ6ZklQHgKc2EM4D3AGqzcPlEd/1jO4730ZTmPsbszazXKUw839WAl5CPPRdb8q/L2p4QnvMnhEUig",
"wAZn5zEiNARoDezCGjThOkKRJge8dWV+pJtq6on0zBAgPPm5MAbwAWA1VvacoLJ/zOfxWvoyn8fY3em1", "RI0b8psDQlCprwZZK+QBZFBwkQdjgKfySp1ZrUHipUspKB9n9ic8TdiAFiDTkcmWkDILzHKzuZqPx6pV",
"6sxTDzdVYyXkI09FUv+r0uZRwhPepPAIlKBEjRvSmwKCN1NfDbImkV/QksNTfqXOCq1B4oV1yZs+Vux3", "3is7QdfoUtEvTiNysJxkKnkxo3AuqtY8qqvQA6oJMlSIAeoNhHX4gSEuYgZSqwVu/yEYG72oBwGnFu8d",
"eJqQAS1AxiOdLSFlVujlZnM1HbdVq7QXdoKuzktFT5xaZG84599ErFWe60gHqNpIXyAGqNcQVuZ7mriA", "+DRm+UdEZZcIM2BMp2sljJfiD6jnZiHvlEvW05XJvy0YYuIjaOPHtvcnq5MVgaNKkLwULGEfT96frKj/",
"GYgLLXD/F8FY54t6EHBa4L0Fn9ost0RUto4wA8Z0qlbEeC5+g6pvFvJOWWcdXZn8016mA/YI2ri27f3J", "cbx3YcQO0vhO5BD7GdjxVBknDmKrGyZ/z1jC3AHlizP6TTjyE65g8FeVbfcG0cLmKEquMSZCvMs4BmZl",
"5mRD4KgcJM8Fi9jHk/cnG3vhwntrRmghDRsW7MBmBdHUdpG/JixidD84J4nPe3tJYQf3rw+bzRhxGrlw", "8trjzK2QXG+Dg19gMu6dAT+sVmPkbe3ivSm/WxqWXPeLcj3kXbWmN2rIWuFsYAwrOlddNOR4cbCDk5lL",
"cNGwbuTKeBSe2Up87gLUTjf241o6A5DwcMBQLjW3fyPrBpJF1/0QXg9ZWm7pjS7A4XMhknIU5nOwKLcg", "+kBp6CNy4Uk9XphlTpLVsQn1D7nfX4D42YqsOlyGC2hgKbnmBSDQHHZdq46ksNOcb1q7foHaQtQBaOqE",
"51zzDBCobbiuSEKRaynicqx/PQ46Hf9UQ739btCMhZ4CVwX+ldz/X7ML5E5ImCIWmWwF3y6perf+shw4", "sn5TYEBuhIQJTAiLc2f44yLSuwPwfOslFzdHt7AcziWCPvNzwStleJzeZp1HQ1dMobuCaimk3ddggkSA",
"F9aXtRHa1BPPN8ubwUy2XAumO5FC6AYY4wB9sfu/iNRTUjsDhKxIUeRcY0gYvEs4emYcpLAH062QXO+9", "9TfgzRg0ArJKEfCdQQ286IM9/ZkZ3Hy4L1SNyuGGXzeZ5Vo9uXQD0AGPX/yAdMSHsHuFVEXs0+rj9Ev9",
"F3bvNO+lXh9MZxbmisVpsgRXEH23NBmZ5qgYAd8Z1MCz/lRnOgqDgY4NYMWe45/+6ptkVvvkk0rb1x3R", "a9ZOiHGuNkKOy/EvZVyol85sKbWMj88lN+ab0tm0lprhr31jMV0NEV69HOHeR2Ddw1tZnAU42Q3i/zS4",
"+MX1fQt40p2MlQH7tPk4/VJ/etwxMWx+MvCn1B/KWFMvrNiCkuDOL9fxdPNyT3tZtO35rQqc5TjJDez/", "fWLGpikY81O99bER72LUsBEGPX8PR/m5sXxTZuwWPwRX558wAoeL1m/Xy4/NtYMTyJqasgH92LR9q3OW",
"NBhuMVPEMRjzQ3X0UotbG7s/iRy38rKWXBChRsvbCdLR2relqmJAP9Z1q9Api1hI3X+5Lf8NAAD//0cV", "sJhOGdW6+j8AAP//fdZwRK4ZAAA=",
"3sY5HAAA",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file

View file

@ -132,36 +132,14 @@ type UploadResponse struct {
// UserResponse defines model for userResponse. // UserResponse defines model for userResponse.
type UserResponse = UserView type UserResponse = UserView
// EnterCodeRequest defines model for enterCodeRequest. // AdminUploadFileMultipartBody defines parameters for AdminUploadFile.
type EnterCodeRequest struct { type AdminUploadFileMultipartBody interface{}
Code string `json:"code"`
}
// GameEditRequest defines model for gameEditRequest.
type GameEditRequest = GameEdit
// Login defines model for login.
type Login struct {
Email string `json:"email"`
Password string `json:"password"`
}
// Register defines model for register.
type Register struct {
Email string `json:"email"`
Password string `json:"password"`
Password2 string `json:"password2"`
Username string `json:"username"`
}
// EnterCodeJSONBody defines parameters for EnterCode. // EnterCodeJSONBody defines parameters for EnterCode.
type EnterCodeJSONBody struct { type EnterCodeJSONBody struct {
Code string `json:"code"` Code string `json:"code"`
} }
// UploadFileMultipartBody defines parameters for UploadFile.
type UploadFileMultipartBody interface{}
// PostUserLoginJSONBody defines parameters for PostUserLogin. // PostUserLoginJSONBody defines parameters for PostUserLogin.
type PostUserLoginJSONBody struct { type PostUserLoginJSONBody struct {
Email string `json:"email"` Email string `json:"email"`
@ -176,18 +154,15 @@ type PostUserRegisterJSONBody struct {
Username string `json:"username"` Username string `json:"username"`
} }
// CreateGameJSONRequestBody defines body for CreateGame for application/json ContentType. // AdminUploadFileMultipartRequestBody defines body for AdminUploadFile for multipart/form-data ContentType.
type CreateGameJSONRequestBody = GameEdit type AdminUploadFileMultipartRequestBody AdminUploadFileMultipartBody
// EditGameJSONRequestBody defines body for EditGame for application/json ContentType. // AdminEditGameJSONRequestBody defines body for AdminEditGame for application/json ContentType.
type EditGameJSONRequestBody = GameEdit type AdminEditGameJSONRequestBody = GameEdit
// EnterCodeJSONRequestBody defines body for EnterCode for application/json ContentType. // EnterCodeJSONRequestBody defines body for EnterCode for application/json ContentType.
type EnterCodeJSONRequestBody EnterCodeJSONBody 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. // PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType.
type PostUserLoginJSONRequestBody PostUserLoginJSONBody type PostUserLoginJSONRequestBody PostUserLoginJSONBody

41
auth.go Normal file
View file

@ -0,0 +1,41 @@
package main
import (
"context"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/labstack/echo/v4"
oapiMiddleware "github.com/oapi-codegen/echo-middleware"
appmiddleware "gitrepo.ru/neonxp/nquest/pkg/contextlib"
"gitrepo.ru/neonxp/nquest/pkg/models"
)
var authFunc = func(ctx context.Context, ai *openapi3filter.AuthenticationInput) error {
echoCtx := ctx.Value(oapiMiddleware.EchoContextKey).(echo.Context)
user := appmiddleware.GetUser(echoCtx)
if user != nil {
if len(ai.Scopes) > 0 {
for _, v := range ai.Scopes {
switch v {
case "user":
return nil
case "creator":
if user.HasRole(models.RoleCreator) {
return nil
}
case "admin":
if user.HasRole(models.RoleAdmin) {
return nil
}
}
}
return echo.ErrForbidden
}
return nil
}
return echo.ErrForbidden
}

View file

@ -77,9 +77,8 @@ function Auth (props) {
if (!user && !baseUser) { if (!user && !baseUser) {
return <Navigate to="/login" state={{ from: location }} replace /> return <Navigate to="/login" state={{ from: location }} replace />
} }
if (props.role && !hasRole(props.role)) { if (props.role && !hasRole(props.role)) {
return <Navigate to="/" replace /> return null
} }
return props.children return props.children

View file

@ -1,3 +1,11 @@
:root {
--accent-color: #fb923c;
--primary-bg: #171E26;
--secondary-bg: #26323f;
accent-color: var(--accent-color);
}
.navbar-brand { .navbar-brand {
padding-top: 0 !important; padding-top: 0 !important;
padding-bottom: 0 !important; padding-bottom: 0 !important;
@ -21,11 +29,11 @@ body,
#container { #container {
height: 100%; height: 100%;
margin: 0; margin: 0;
background-color: #171E26; background-color: var(--primary-bg);
} }
.ant-layout-header { .ant-layout-header {
background-color: #26323f; background-color: var(--secondary-bg);
border: 0; border: 0;
border-bottom: 1px solid rgba(154, 197, 247, 0.19); border-bottom: 1px solid rgba(154, 197, 247, 0.19);
} }

View file

@ -30,11 +30,7 @@ const Quest = () => {
} }
const onFinish = (values) => { const onFinish = (values) => {
let url = '/api/admin/games' ajax('/api/admin/games', {
if (quest.id) {
url = `/api/admin/games/${quest.id}`
}
ajax(url, {
method: 'POST', method: 'POST',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
@ -68,6 +64,9 @@ const Quest = () => {
Сохранить квест Сохранить квест
</Button> </Button>
</Form.Item> </Form.Item>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
<Form.Item label='Опубликован?' name='visible'> <Form.Item label='Опубликован?' name='visible'>
<Switch /> <Switch />
</Form.Item> </Form.Item>
@ -83,7 +82,7 @@ const Quest = () => {
getValueFromEvent={normFile} getValueFromEvent={normFile}
> >
{quest.icon ? <Avatar src={`/api/file/${quest.icon}`} /> : null} {quest.icon ? <Avatar src={`/api/file/${quest.icon}`} /> : null}
<Upload name='file' action='/api/file/upload' listType='picture' maxCount={1}> <Upload name='file' action='/api/admin/file/upload' listType='picture' maxCount={1}>
<Button icon={<UploadOutlined />}>Загрузка</Button> <Button icon={<UploadOutlined />}>Загрузка</Button>
</Upload> </Upload>
</Form.Item> </Form.Item>

View file

@ -12,6 +12,7 @@ const Quests = () => {
<Link to="/admin/quests/new">Создать новый квест</Link> <Link to="/admin/quests/new">Создать новый квест</Link>
<Table <Table
dataSource={quests} dataSource={quests}
rowKey={'id'}
columns={[ columns={[
{ {
title: 'UUID', title: 'UUID',

54
main.go
View file

@ -1,7 +1,6 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"time" "time"
@ -25,9 +24,7 @@ import (
"gitrepo.ru/neonxp/nquest/pkg/service" "gitrepo.ru/neonxp/nquest/pkg/service"
) )
var ( var Version = "dev"
Version = "dev"
)
func main() { func main() {
cfg, err := GetConfig() cfg, err := GetConfig()
@ -41,22 +38,12 @@ func main() {
fmt.Fprintf(os.Stderr, "Error DB connection\n: %s", err) fmt.Fprintf(os.Stderr, "Error DB connection\n: %s", err)
os.Exit(1) os.Exit(1)
} }
// db.Use(prometheus.New(prometheus.Config{
// DBName: "db1", // use `DBName` as metrics label
// RefreshInterval: 15, // Refresh metrics interval (default 15 seconds)
// MetricsCollector: []prometheus.MetricsCollector{
// &prometheus.MySQL{
// VariableNames: []string{"Threads_running"},
// },
// }, // user defined metrics
// }))
if err := db.AutoMigrate( if err := db.AutoMigrate(
&models.User{}, &models.User{},
&models.Game{}, &models.Game{},
&models.GameCursor{}, &models.GameCursor{},
&models.Task{}, &models.Task{},
&models.Solution{},
&models.Code{}, &models.Code{},
&models.File{}, &models.File{},
); err != nil { ); err != nil {
@ -82,37 +69,6 @@ func main() {
}() }()
go store.PeriodicCleanup(12*time.Hour, quit) go store.PeriodicCleanup(12*time.Hour, quit)
// userMW := appmiddleware.User(models.RoleUser, userService)
authFunc := func(ctx context.Context, ai *openapi3filter.AuthenticationInput) error {
echoCtx := ctx.Value(oapiMiddleware.EchoContextKey).(echo.Context)
user := appmiddleware.GetUser(echoCtx)
if user != nil {
if len(ai.Scopes) > 0 {
for _, v := range ai.Scopes {
switch v {
case "user":
return nil
case "creator":
if user.HasRole(models.RoleCreator) {
return nil
}
case "admin":
if user.HasRole(models.RoleAdmin) {
return nil
}
}
}
return echo.ErrForbidden
}
return nil
}
return echo.ErrForbidden
}
swagger, err := api.GetSwagger() swagger, err := api.GetSwagger()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err) fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err)
@ -127,14 +83,6 @@ func main() {
session.Middleware(store), session.Middleware(store),
middleware.Logger(), middleware.Logger(),
middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)), middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)),
// middleware.CSRFWithConfig(middleware.CSRFConfig{
// TokenLookup: "cookie:_csrf",
// CookiePath: "/",
// // CookieDomain: "nquest.ru",
// // CookieSecure: true,
// CookieHTTPOnly: true,
// CookieSameSite: http.SameSiteStrictMode,
// }),
middleware.Gzip(), middleware.Gzip(),
echoprometheus.NewMiddleware("nquest"), echoprometheus.NewMiddleware("nquest"),
appmiddleware.User(userService), appmiddleware.User(userService),

View file

@ -16,78 +16,18 @@ type Admin struct {
GameService *service.Game GameService *service.Game
} }
// (POST /admin/games) // (POST /games/{uid})
func (a *Admin) CreateGame(ctx echo.Context) error { func (a *Admin) AdminEditGame(ctx echo.Context) error {
user := contextlib.GetUser(ctx) user := contextlib.GetUser(ctx)
req := &api.GameEditRequest{} req := &api.AdminEditGameJSONRequestBody{}
if err := ctx.Bind(req); err != nil { if err := ctx.Bind(req); err != nil {
return err return err
} }
game := a.mapCreateGameRequest(req, user) game := a.mapCreateGameRequest(req, user)
var err error game, err := a.GameService.UpsertGame(ctx.Request().Context(), game)
game, err = a.GameService.CreateGame(ctx.Request().Context(), game)
if err != nil {
return err
}
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,
Points: game.Points,
Tasks: tasks,
Title: game.Title,
Type: api.MapGameTypeReverse(game.Type),
Visible: game.Visible,
})
}
// (POST /games/{uid})
func (a *Admin) EditGame(ctx echo.Context, uid uuid.UUID) error {
user := contextlib.GetUser(ctx)
req := &api.GameEditRequest{}
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, err = a.GameService.UpdateGame(ctx.Request().Context(), uid, game)
if err != nil { if err != nil {
return err return err
} }
@ -125,7 +65,7 @@ func (a *Admin) EditGame(ctx echo.Context, uid uuid.UUID) error {
} }
// (GET /games/{uid}) // (GET /games/{uid})
func (a *Admin) GetGameByAdmin(ctx echo.Context, uid uuid.UUID) error { func (a *Admin) AdminGetGame(ctx echo.Context, uid uuid.UUID) error {
user := contextlib.GetUser(ctx) user := contextlib.GetUser(ctx)
game, err := a.GameService.GetByID(ctx.Request().Context(), uid) game, err := a.GameService.GetByID(ctx.Request().Context(), uid)
@ -177,7 +117,7 @@ func (a *Admin) GetGameByAdmin(ctx echo.Context, uid uuid.UUID) error {
}) })
} }
func (a *Admin) ListGamesByAdmin(ctx echo.Context) error { func (a *Admin) AdminListGames(ctx echo.Context) error {
user := contextlib.GetUser(ctx) user := contextlib.GetUser(ctx)
games, err := a.GameService.ListByAuthor(ctx.Request().Context(), user) games, err := a.GameService.ListByAuthor(ctx.Request().Context(), user)
@ -202,11 +142,13 @@ func (a *Admin) ListGamesByAdmin(ctx echo.Context) error {
return ctx.JSON(http.StatusOK, resp) return ctx.JSON(http.StatusOK, resp)
} }
func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) *models.Game { func (*Admin) mapCreateGameRequest(req *api.GameEdit, user *models.User) *models.Game {
id := uuid.New()
if req.Id != nil {
id = *req.Id
}
game := &models.Game{ game := &models.Game{
Model: models.Model{ ID: id,
ID: uuid.New(),
},
Visible: req.Visible, Visible: req.Visible,
Title: req.Title, Title: req.Title,
Description: req.Description, Description: req.Description,
@ -219,14 +161,12 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User)
IconID: req.Icon, IconID: req.Icon,
} }
for order, te := range req.Tasks { for order, te := range req.Tasks {
if te.Id == nil { id := uuid.New()
u := uuid.New() if te.Id != nil {
te.Id = &u id = *te.Id
} }
task := &models.Task{ task := &models.Task{
Model: models.Model{ ID: id,
ID: *te.Id,
},
Title: te.Title, Title: te.Title,
Text: te.Text, Text: te.Text,
Codes: make([]*models.Code, 0, len(te.Codes)), Codes: make([]*models.Code, 0, len(te.Codes)),
@ -234,14 +174,12 @@ func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User)
} }
for _, ce := range te.Codes { for _, ce := range te.Codes {
if ce.Id == nil { id := uuid.New()
u := uuid.New() if ce.Id != nil {
ce.Id = &u id = *ce.Id
} }
task.Codes = append(task.Codes, &models.Code{ task.Codes = append(task.Codes, &models.Code{
Model: models.Model{ ID: id,
ID: *ce.Id,
},
Code: ce.Code, Code: ce.Code,
Description: ce.Description, Description: ce.Description,
}) })

View file

@ -12,7 +12,7 @@ type File struct {
} }
// (POST /file/upload) // (POST /file/upload)
func (u *File) UploadFile(c echo.Context) error { func (u *File) AdminUploadFile(c echo.Context) error {
// user := contextlib.GetUser(c) // user := contextlib.GetUser(c)
fh, err := c.FormFile("file") fh, err := c.FormFile("file")
if err != nil { if err != nil {

View file

@ -1,10 +1,19 @@
package models package models
type File struct { import (
Model "time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type File struct {
ID uuid.UUID `gorm:"primarykey" json:"id"`
Filename string Filename string
ContentType string ContentType string
Size int Size int
Body []byte `gorm:"type:bytea"` Body []byte `gorm:"type:bytea"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
} }

View file

@ -1,11 +1,15 @@
package models package models
import "github.com/google/uuid" import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Game struct { type Game struct {
Model ID uuid.UUID `gorm:"primarykey" json:"id"`
Visible bool `gorm:"index"`
Visible bool `gorm:"index"`
Title string Title string
Description string Description string
Tasks []*Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` Tasks []*Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
@ -14,6 +18,9 @@ type Game struct {
Points int Points int
Icon *File Icon *File
IconID uuid.UUID IconID uuid.UUID
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
} }
type GameType int type GameType int

View file

@ -1,15 +0,0 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Model struct {
ID uuid.UUID `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

View file

@ -1,29 +1,22 @@
package models package models
import "github.com/google/uuid" import (
"github.com/google/uuid"
)
type Task struct { type Task struct {
Model ID uuid.UUID `gorm:"primarykey" json:"id"`
Title string Title string
Text string Text string
MaxTime int MaxTime int
GameID uuid.UUID GameID uuid.UUID
Solutions []*Solution `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
TaskOrder uint TaskOrder uint
} }
type Solution struct {
Model
TaskID uuid.UUID
After int
Text string
}
type Code struct { type Code struct {
Model ID uuid.UUID `gorm:"primarykey" json:"id"`
TaskID uuid.UUID TaskID uuid.UUID
Code string `gorm:"index"` Code string `gorm:"index"`

View file

@ -2,15 +2,22 @@ package models
import ( import (
"errors" "errors"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
) )
var ErrEmptyPassword = errors.New("empty password") var ErrEmptyPassword = errors.New("empty password")
type User struct { type User struct {
Model ID uuid.UUID `gorm:"primarykey" json:"id"`
Username string `gorm:"unique" json:"username"` CreatedAt time.Time `json:"-"`
Email string `gorm:"unique" json:"email"` UpdatedAt time.Time `json:"-"`
Password string `json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Username string `gorm:"unique" json:"username"`
Email string `gorm:"unique" json:"email"`
Password string `json:"-"`
Experience int Experience int
Games []*GameCursor Games []*GameCursor
Role UserRole Role UserRole

View file

@ -137,7 +137,6 @@ func (e *Engine) GetNext(ctx context.Context, gameID uuid.UUID, currentOrder uin
var t models.Task var t models.Task
err := e.DB.WithContext(ctx). err := e.DB.WithContext(ctx).
Preload("Codes"). Preload("Codes").
Preload("Solutions").
Order("task_order ASC"). Order("task_order ASC").
First(&t, `game_id = ? AND task_order > ?`, gameID, currentOrder). First(&t, `game_id = ? AND task_order > ?`, gameID, currentOrder).
Error Error

View file

@ -32,7 +32,7 @@ func (u *File) Upload(
} }
file := &models.File{ file := &models.File{
Model: models.Model{ID: uuid.New()}, ID: uuid.New(),
Filename: filename, Filename: filename,
ContentType: contentType, ContentType: contentType,
Size: size, Size: size,

View file

@ -28,7 +28,6 @@ func (gs *Game) GetByID(ctx context.Context, id uuid.UUID) (*models.Game, error)
return db.Order("tasks.task_order ASC") return db.Order("tasks.task_order ASC")
}). }).
Preload("Tasks.Codes"). Preload("Tasks.Codes").
Preload("Tasks.Solutions").
First(g, id). First(g, id).
Error Error
} }
@ -67,20 +66,9 @@ func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*model
Error Error
} }
func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game, error) { func (gs *Game) UpsertGame(ctx context.Context, game *models.Game) (*models.Game, error) {
return game, gs.DB. return game, gs.DB.Debug().
Session(&gorm.Session{FullSaveAssociations: true}). Session(&gorm.Session{FullSaveAssociations: true}).
Create(game).
Error
}
func (gs *Game) UpdateGame(ctx context.Context, uid uuid.UUID, game *models.Game) (*models.Game, error) {
game.ID = uid
db := gs.DB
return game, db.Debug().
Session(&gorm.Session{FullSaveAssociations: true}).Omit("created_at").
Save(game). Save(game).
Error Error
} }

View file

@ -59,9 +59,7 @@ func (s *User) Register(ctx context.Context, username, email, password, password
} }
u := &models.User{ u := &models.User{
Model: models.Model{ ID: uuid.New(),
ID: uuid.New(),
},
Username: username, Username: username,
Email: normalizer.NewNormalizer().Normalize(email), Email: normalizer.NewNormalizer().Normalize(email),
Password: hex.EncodeToString(hashed), Password: hex.EncodeToString(hashed),

View file

@ -252,4 +252,4 @@ Content-Type: image/jpg
### ###
GET http://localhost:8000/api/file/f343919a-b068-4d72-ade5-bbb84db0bec9 GET http://localhost:8000/api/file/f343919a-b068-4d72-ade5-bbb84db0bec9