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",
"mode": "auto",
"program": "${workspaceFolder}/",
"envFile": "${workspaceFolder}/.env"
"envFile": "${workspaceFolder}/.devcontainer/.env"
}
]
}

View file

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

View file

@ -22,17 +22,17 @@ import (
// ServerInterface represents all server handlers.
type ServerInterface interface {
// (POST /admin/file/upload)
AdminUploadFile(ctx echo.Context) error
// (GET /admin/games)
ListGamesByAdmin(ctx echo.Context) error
AdminListGames(ctx echo.Context) error
// (POST /admin/games)
CreateGame(ctx echo.Context) error
AdminEditGame(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
AdminGetGame(ctx echo.Context, uid openapi_types.UUID) error
// (GET /engine/{uid})
GameEngine(ctx echo.Context, uid openapi_types.UUID) error
@ -40,9 +40,6 @@ type ServerInterface interface {
// (POST /engine/{uid}/code)
EnterCode(ctx echo.Context, uid openapi_types.UUID) error
// (POST /file/upload)
UploadFile(ctx echo.Context) error
// (GET /file/{uid})
GetFile(ctx echo.Context, uid openapi_types.UUID) error
@ -67,30 +64,41 @@ type ServerInterfaceWrapper struct {
Handler ServerInterface
}
// ListGamesByAdmin converts echo context to params.
func (w *ServerInterfaceWrapper) ListGamesByAdmin(ctx echo.Context) error {
// AdminUploadFile converts echo context to params.
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
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.ListGamesByAdmin(ctx)
err = w.Handler.AdminListGames(ctx)
return err
}
// CreateGame converts echo context to params.
func (w *ServerInterfaceWrapper) CreateGame(ctx echo.Context) error {
// AdminEditGame converts echo context to params.
func (w *ServerInterfaceWrapper) AdminEditGame(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)
err = w.Handler.AdminEditGame(ctx)
return err
}
// GetGameByAdmin converts echo context to params.
func (w *ServerInterfaceWrapper) GetGameByAdmin(ctx echo.Context) error {
// AdminGetGame converts echo context to params.
func (w *ServerInterfaceWrapper) AdminGetGame(ctx echo.Context) error {
var err error
// ------------- Path parameter "uid" -------------
var uid openapi_types.UUID
@ -103,25 +111,7 @@ func (w *ServerInterfaceWrapper) GetGameByAdmin(ctx echo.Context) error {
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)
err = w.Handler.AdminGetGame(ctx, uid)
return err
}
@ -161,17 +151,6 @@ func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error {
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.
func (w *ServerInterfaceWrapper) GetFile(ctx echo.Context) error {
var err error
@ -269,13 +248,12 @@ 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.POST(baseURL+"/admin/file/upload", wrapper.AdminUploadFile)
router.GET(baseURL+"/admin/games", wrapper.AdminListGames)
router.POST(baseURL+"/admin/games", wrapper.AdminEditGame)
router.GET(baseURL+"/admin/games/:uid", wrapper.AdminGetGame)
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.GET(baseURL+"/user", wrapper.GetUser)
@ -288,27 +266,26 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"H4sIAAAAAAAC/8xY32+kNhD+Vyq3j1zY+/HEWxqlUdWoaq+5vkSryIHJxhewqT3OZRXxv1djAwuLWcge",
"Su4pGzx4Zr75vmHsZ5aqolQSJBqWPDMNplTSgPsHtFb6c/2EHqRKIkikn7wsc5FyFErGX42S9Myk91Bw",
"+lVqVYJG4fdJVeZex20JLGFCImxAsypiBRjDN91Fg1rIDauqiGn4zwoNGUuu/RY7+3XU2Kvbr5Aiq+iF",
"DEyqRUkxsYTR/htewGlWCHlUFr9ouGMJ+zneYRT7VRPTzueZOOj5Uhg8yrFAKMycCP4V8I281WBwrfl2",
"LCLk5mFxGGhTH0TYqS1zxbMFOGStyOjvndIFR5b4B9EEbZzRXLJYA3pxgGjTcYCqqN6m1Ynj1JSCmmz3",
"NgysHwNad8/Iux5C6BdcZktFeyCMkP9WgQP/k6CkfmEClpnoRaxUom6fww5HAjGzNU3WvqvsazpiKDAP",
"w+ofTHeLK7KrIvYojLjt7XWrVA5cDmrQWDbeoz1yuNebJFsgaoTHinZVhwvSFt6JRstz4prAbee1XYZt",
"rxvUmlu8V3o+wjs9DhFONXCE7BSPk9ors+pMWYkjywtRZY8NPs5DRGgJsAuwC2vUlusARVoNBPvK/Eq3",
"3TRQ6ZklQHgKc2EM4D3AGqzcPlEd/1jO4730ZTmPsbszazXKUw839WAl5CPPRdb8q/L2p4QnvMnhEUig",
"RI0b8psDQlCprwZZK+QBZFBwkQdjgKfySp1ZrUHipUspKB9n9ic8TdiAFiDTkcmWkDILzHKzuZqPx6pV",
"3is7QdfoUtEvTiNysJxkKnkxo3AuqtY8qqvQA6oJMlSIAeoNhHX4gSEuYgZSqwVu/yEYG72oBwGnFu8d",
"+DRm+UdEZZcIM2BMp2sljJfiD6jnZiHvlEvW05XJvy0YYuIjaOPHtvcnq5MVgaNKkLwULGEfT96frKj/",
"cbx3YcQO0vhO5BD7GdjxVBknDmKrGyZ/z1jC3AHlizP6TTjyE65g8FeVbfcG0cLmKEquMSZCvMs4BmZl",
"8trjzK2QXG+Dg19gMu6dAT+sVmPkbe3ivSm/WxqWXPeLcj3kXbWmN2rIWuFsYAwrOlddNOR4cbCDk5lL",
"+kBp6CNy4Uk9XphlTpLVsQn1D7nfX4D42YqsOlyGC2hgKbnmBSDQHHZdq46ksNOcb1q7foHaQtQBaOqE",
"sn5TYEBuhIQJTAiLc2f44yLSuwPwfOslFzdHt7AcziWCPvNzwStleJzeZp1HQ1dMobuCaimk3ddggkSA",
"9TfgzRg0ArJKEfCdQQ286IM9/ZkZ3Hy4L1SNyuGGXzeZ5Vo9uXQD0AGPX/yAdMSHsHuFVEXs0+rj9Ev9",
"a9ZOiHGuNkKOy/EvZVyol85sKbWMj88lN+ab0tm0lprhr31jMV0NEV69HOHeR2Ddw1tZnAU42Q3i/zS4",
"fWLGpikY81O99bER72LUsBEGPX8PR/m5sXxTZuwWPwRX558wAoeL1m/Xy4/NtYMTyJqasgH92LR9q3OW",
"sJhOGdW6+j8AAP//fdZwRK4ZAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file

View file

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

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) {
return <Navigate to="/login" state={{ from: location }} replace />
}
if (props.role && !hasRole(props.role)) {
return <Navigate to="/" replace />
return null
}
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 {
padding-top: 0 !important;
padding-bottom: 0 !important;
@ -21,11 +29,11 @@ body,
#container {
height: 100%;
margin: 0;
background-color: #171E26;
background-color: var(--primary-bg);
}
.ant-layout-header {
background-color: #26323f;
background-color: var(--secondary-bg);
border: 0;
border-bottom: 1px solid rgba(154, 197, 247, 0.19);
}

View file

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

View file

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

54
main.go
View file

@ -1,7 +1,6 @@
package main
import (
"context"
"fmt"
"os"
"time"
@ -25,9 +24,7 @@ import (
"gitrepo.ru/neonxp/nquest/pkg/service"
)
var (
Version = "dev"
)
var Version = "dev"
func main() {
cfg, err := GetConfig()
@ -41,22 +38,12 @@ func main() {
fmt.Fprintf(os.Stderr, "Error DB connection\n: %s", err)
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(
&models.User{},
&models.Game{},
&models.GameCursor{},
&models.Task{},
&models.Solution{},
&models.Code{},
&models.File{},
); err != nil {
@ -82,37 +69,6 @@ func main() {
}()
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()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err)
@ -127,14 +83,6 @@ func main() {
session.Middleware(store),
middleware.Logger(),
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(),
echoprometheus.NewMiddleware("nquest"),
appmiddleware.User(userService),

View file

@ -16,78 +16,18 @@ type Admin struct {
GameService *service.Game
}
// (POST /admin/games)
func (a *Admin) CreateGame(ctx echo.Context) error {
// (POST /games/{uid})
func (a *Admin) AdminEditGame(ctx echo.Context) error {
user := contextlib.GetUser(ctx)
req := &api.GameEditRequest{}
req := &api.AdminEditGameJSONRequestBody{}
if err := ctx.Bind(req); err != nil {
return err
}
game := a.mapCreateGameRequest(req, user)
var err error
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)
game, err := a.GameService.UpsertGame(ctx.Request().Context(), game)
if err != nil {
return err
}
@ -125,7 +65,7 @@ func (a *Admin) EditGame(ctx echo.Context, uid uuid.UUID) error {
}
// (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)
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)
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)
}
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{
Model: models.Model{
ID: uuid.New(),
},
ID: id,
Visible: req.Visible,
Title: req.Title,
Description: req.Description,
@ -219,14 +161,12 @@ 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
id := uuid.New()
if te.Id != nil {
id = *te.Id
}
task := &models.Task{
Model: models.Model{
ID: *te.Id,
},
ID: id,
Title: te.Title,
Text: te.Text,
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 {
if ce.Id == nil {
u := uuid.New()
ce.Id = &u
id := uuid.New()
if ce.Id != nil {
id = *ce.Id
}
task.Codes = append(task.Codes, &models.Code{
Model: models.Model{
ID: *ce.Id,
},
ID: id,
Code: ce.Code,
Description: ce.Description,
})

View file

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

View file

@ -1,10 +1,19 @@
package models
type File struct {
Model
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type File struct {
ID uuid.UUID `gorm:"primarykey" json:"id"`
Filename string
ContentType string
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
import "github.com/google/uuid"
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Game struct {
Model
Visible bool `gorm:"index"`
ID uuid.UUID `gorm:"primarykey" json:"id"`
Visible bool `gorm:"index"`
Title string
Description string
Tasks []*Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
@ -14,6 +18,9 @@ type Game struct {
Points int
Icon *File
IconID uuid.UUID
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
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
import "github.com/google/uuid"
import (
"github.com/google/uuid"
)
type Task struct {
Model
ID uuid.UUID `gorm:"primarykey" json:"id"`
Title string
Text string
MaxTime int
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
}
type Solution struct {
Model
TaskID uuid.UUID
After int
Text string
}
type Code struct {
Model
ID uuid.UUID `gorm:"primarykey" json:"id"`
TaskID uuid.UUID
Code string `gorm:"index"`

View file

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

View file

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

View file

@ -32,7 +32,7 @@ func (u *File) Upload(
}
file := &models.File{
Model: models.Model{ID: uuid.New()},
ID: uuid.New(),
Filename: filename,
ContentType: contentType,
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")
}).
Preload("Tasks.Codes").
Preload("Tasks.Solutions").
First(g, id).
Error
}
@ -67,20 +66,9 @@ func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*model
Error
}
func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game, error) {
return game, gs.DB.
func (gs *Game) UpsertGame(ctx context.Context, game *models.Game) (*models.Game, error) {
return game, gs.DB.Debug().
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).
Error
}

View file

@ -59,9 +59,7 @@ func (s *User) Register(ctx context.Context, username, email, password, password
}
u := &models.User{
Model: models.Model{
ID: uuid.New(),
},
ID: uuid.New(),
Username: username,
Email: normalizer.NewNormalizer().Normalize(email),
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