Рефакторинг api
This commit is contained in:
parent
439ba77812
commit
2e1db4ac60
29 changed files with 865 additions and 1555 deletions
|
@ -1,100 +0,0 @@
|
||||||
FROM ubuntu
|
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-euo", "pipefail", "-c"]
|
|
||||||
|
|
||||||
# Create system wide environment as root
|
|
||||||
|
|
||||||
## Supports linux/amd64 or linux/arm64
|
|
||||||
ARG TARGETPLATFORM
|
|
||||||
|
|
||||||
## Ensure installation does not prompt for input
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
## Install basic tools
|
|
||||||
RUN apt-get -yq update && \
|
|
||||||
apt-get -yq upgrade && \
|
|
||||||
apt-get -yq install \
|
|
||||||
git \
|
|
||||||
vim \
|
|
||||||
tmux \
|
|
||||||
curl \
|
|
||||||
wget \
|
|
||||||
build-essential \
|
|
||||||
cmake \
|
|
||||||
gcc \
|
|
||||||
shellcheck \
|
|
||||||
unzip \
|
|
||||||
tree \
|
|
||||||
software-properties-common \
|
|
||||||
jq \
|
|
||||||
gettext-base \
|
|
||||||
uuid-runtime \
|
|
||||||
postgresql-client \
|
|
||||||
sqlite3 \
|
|
||||||
pandoc \
|
|
||||||
texlive \
|
|
||||||
texlive-latex-extra \
|
|
||||||
wkhtmltopdf \
|
|
||||||
htop
|
|
||||||
|
|
||||||
## Install latest go
|
|
||||||
RUN GO_VERSION="$(git ls-remote https://github.com/golang/go | grep -oE "refs/tags/go[0-9]+\.[0-9]+(\.[0-9])?$" | sed 's|refs/tags/go||g' | sort --version-sort | tail -n 1)" && \
|
|
||||||
ARCH=$(basename "${TARGETPLATFORM}") && \
|
|
||||||
curl -fsSL "https://dl.google.com/go/go${GO_VERSION}.linux-${ARCH}.tar.gz" | tar -xz -C /usr/local
|
|
||||||
|
|
||||||
# Customize environment for nonroot user
|
|
||||||
ARG USERNAME=nonroot
|
|
||||||
ENV HOME=/home/nonroot
|
|
||||||
ARG USER_UID=1000
|
|
||||||
ARG USER_GID=1000
|
|
||||||
RUN groupadd --gid "${USER_GID}" "${USERNAME}" && \
|
|
||||||
useradd --uid "${USER_UID}" --gid "${USER_GID}" --create-home "${USERNAME}" && \
|
|
||||||
apt-get update && \
|
|
||||||
apt-get -yq install sudo && \
|
|
||||||
echo "${USERNAME}" ALL=\(root\) NOPASSWD:ALL > "/etc/sudoers.d/${USERNAME}" && \
|
|
||||||
chmod 0440 "/etc/sudoers.d/${USERNAME}" && \
|
|
||||||
usermod -aG docker "${USERNAME}"
|
|
||||||
USER "${USERNAME}:${USERNAME}"
|
|
||||||
|
|
||||||
## Delete default configs
|
|
||||||
RUN rm "${HOME}/.profile" "${HOME}/.bashrc" && \
|
|
||||||
touch "${HOME}/.bashrc"
|
|
||||||
|
|
||||||
## Install latest node
|
|
||||||
RUN curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh" | PROFILE="${HOME}/.bashrc" bash && \
|
|
||||||
source "${HOME}/.bashrc" && \
|
|
||||||
nvm install node && \
|
|
||||||
nvm use node
|
|
||||||
|
|
||||||
|
|
||||||
## Install go packages
|
|
||||||
### Note: coc-go installs gopls when using vim - instead of coc-go, just install gopls so it is ready on container startup.
|
|
||||||
### Other coc-* extensions behave much better.
|
|
||||||
ENV GOROOT="/usr/local/go"
|
|
||||||
ENV GOPATH="${HOME}/go"
|
|
||||||
ENV PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}"
|
|
||||||
RUN go install golang.org/x/tools/gopls@latest && \
|
|
||||||
go install github.com/go-delve/delve/cmd/dlv@latest
|
|
||||||
|
|
||||||
## Customize tmux
|
|
||||||
RUN echo "set-option -g default-command /bin/bash" > "${HOME}/.tmux.conf" && \
|
|
||||||
echo "set-option -g mouse on" >> "${HOME}/.tmux.conf"
|
|
||||||
|
|
||||||
## Customize bashrc
|
|
||||||
RUN echo "export TERM=xterm-color" >> "${HOME}/.bashrc" && \
|
|
||||||
sudo cat /root/.bashrc >> "${HOME}/.bashrc" && \
|
|
||||||
echo "set -o vi" >> "${HOME}/.bashrc" && \
|
|
||||||
echo "alias tmux='tmux -u'" >> "${HOME}/.bashrc" && \
|
|
||||||
echo "export PATH=${PATH}" >> "${HOME}/.bashrc" && \
|
|
||||||
echo "nvm use node > /dev/null 2>&1" >> "${HOME}/.bashrc"
|
|
||||||
|
|
||||||
## Customize .profile
|
|
||||||
RUN sudo cat /root/.profile >> "${HOME}/.profile"
|
|
||||||
|
|
||||||
## Cleanup
|
|
||||||
RUN sudo apt-get clean && \
|
|
||||||
sudo rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Mimic VSCode's workspace
|
|
||||||
RUN mkdir -p "${HOME}/workspaces/dev-container"
|
|
||||||
WORKDIR "${HOME}/workspaces/dev-container"
|
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Dev Dockerfile",
|
|
||||||
"dockerFile": "Dockerfile",
|
|
||||||
"settings": {
|
|
||||||
"remoteUser": "nonroot",
|
|
||||||
"files.eol": "\n",
|
|
||||||
"terminal.integrated.profiles.linux": {
|
|
||||||
"bash": {
|
|
||||||
"path": "bash",
|
|
||||||
"icon": "terminal-bash"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"terminal.integrated.defaultProfile.linux": "bash",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"[go]": {
|
|
||||||
"editor.defaultFormatter": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"extensions": [
|
|
||||||
"golang.go",
|
|
||||||
"ms-azuretools.vscode-docker",
|
|
||||||
"esbenp.prettier-vscode"
|
|
||||||
]
|
|
||||||
}
|
|
10
.env
10
.env
|
@ -1,7 +1,7 @@
|
||||||
PG_HOST=localhost
|
POSTGRES_HOSTNAME=localhost
|
||||||
PG_NAME=nquest
|
POSTGRES_DB=nquest
|
||||||
PG_USER=nquest
|
POSTGRES_USER=nquest
|
||||||
PG_PASS=nquest
|
POSTGRES_PASSWORD=nquest
|
||||||
PG_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
SECRET=s3cr3t
|
SECRET=s3cr3t
|
||||||
LISTEN=:8000
|
LISTEN=:8000
|
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
// Используйте IntelliSense, чтобы узнать о возможных атрибутах.
|
||||||
|
// Наведите указатель мыши, чтобы просмотреть описания существующих атрибутов.
|
||||||
|
// Для получения дополнительной информации посетите: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Package",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/",
|
||||||
|
"envFile": "${workspaceFolder}/.env"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
14
.vscode/settings.json
vendored
Normal file
14
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"sqltools.connections": [
|
||||||
|
{
|
||||||
|
"previewLimit": 50,
|
||||||
|
"server": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"driver": "PostgreSQL",
|
||||||
|
"name": "localhost",
|
||||||
|
"database": "nquest",
|
||||||
|
"username": "nquest",
|
||||||
|
"password": "nquest"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,38 +1,25 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import "gitrepo.ru/neonxp/nquest/pkg/models"
|
import (
|
||||||
|
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
var MapTeamRole = map[models.Role]UserTeamRole{
|
func MapGameType(typ GameType) models.GameType {
|
||||||
models.Captain: Captain,
|
switch typ {
|
||||||
models.Member: Member,
|
case City:
|
||||||
}
|
return models.CityGame
|
||||||
var MapTeamRoleReverse = map[UserTeamRole]models.Role{
|
case Virtual:
|
||||||
Captain: models.Captain,
|
return models.VirtualGame
|
||||||
Member: models.Member,
|
|
||||||
}
|
|
||||||
|
|
||||||
var MapUserRole = map[models.UserRole]UserRole{
|
|
||||||
models.RoleNotVerified: NotVerified,
|
|
||||||
models.RoleUser: User,
|
|
||||||
models.RoleCreator: Creator,
|
|
||||||
models.RoleAdmin: Admin,
|
|
||||||
}
|
|
||||||
|
|
||||||
var MapUserRoleReverse = map[UserRole]models.UserRole{
|
|
||||||
NotVerified: models.RoleNotVerified,
|
|
||||||
User: models.RoleUser,
|
|
||||||
Creator: models.RoleCreator,
|
|
||||||
Admin: models.RoleAdmin,
|
|
||||||
}
|
|
||||||
|
|
||||||
func MapUserTeam(team *models.TeamMember) *UserTeam {
|
|
||||||
if team == nil || team.Team == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &UserTeam{
|
|
||||||
Id: int(team.Team.ID),
|
|
||||||
Name: team.Team.Name,
|
|
||||||
Role: MapTeamRole[team.Role],
|
|
||||||
}
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapGameTypeReverse(typ models.GameType) GameType {
|
||||||
|
switch typ {
|
||||||
|
case models.CityGame:
|
||||||
|
return City
|
||||||
|
case models.VirtualGame:
|
||||||
|
return Virtual
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
411
api/openapi.yaml
411
api/openapi.yaml
|
@ -44,188 +44,52 @@ paths:
|
||||||
description: "success logout"
|
description: "success logout"
|
||||||
400:
|
400:
|
||||||
$ref: '#/components/responses/errorResponse'
|
$ref: '#/components/responses/errorResponse'
|
||||||
# Team routes
|
|
||||||
/teams:
|
|
||||||
get:
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
$ref: '#/components/responses/teamsListResponse'
|
|
||||||
403:
|
|
||||||
$ref: '#/components/responses/errorResponse'
|
|
||||||
post:
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
'application/json':
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
required: [ name ]
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
$ref: '#/components/responses/teamResponse'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/errorResponse'
|
|
||||||
/teams/{teamID}:
|
|
||||||
get:
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: teamID
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
$ref: '#/components/responses/teamResponse'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/errorResponse'
|
|
||||||
delete:
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: teamID
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
responses:
|
|
||||||
204:
|
|
||||||
description: ''
|
|
||||||
403:
|
|
||||||
$ref: '#/components/responses/errorResponse'
|
|
||||||
/teams/{teamID}/members:
|
|
||||||
post:
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: teamID
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
'application/json':
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
members:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: integer
|
|
||||||
required: [ members ]
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
$ref: '#/components/responses/teamResponse'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/errorResponse'
|
|
||||||
delete:
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: teamID
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
$ref: '#/components/responses/teamResponse'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/errorResponse'
|
|
||||||
/teams/{teamID}/requests/{userID}:
|
|
||||||
post:
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: teamID
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
- in: path
|
|
||||||
name: userID
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
'application/json':
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
approve:
|
|
||||||
type: boolean
|
|
||||||
required: [ approve ]
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
$ref: '#/components/responses/teamResponse'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/errorResponse'
|
|
||||||
/teams/{teamID}/requests:
|
|
||||||
post:
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: teamID
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
$ref: '#/components/responses/teamResponse'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/errorResponse'
|
|
||||||
/admin/games:
|
|
||||||
get:
|
|
||||||
security:
|
|
||||||
- cookieAuth: [creator, admin]
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
$ref: "#/components/responses/gameAdminList"
|
|
||||||
# Game routes
|
|
||||||
|
|
||||||
|
# Game routes
|
||||||
/games:
|
/games:
|
||||||
get:
|
get:
|
||||||
security: []
|
security: []
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
$ref: '#/components/responses/gameListResponse'
|
$ref: '#/components/responses/gameListResponse'
|
||||||
|
post:
|
||||||
|
operationId: createGame
|
||||||
|
security:
|
||||||
|
- cookieAuth: [creator, admin]
|
||||||
|
requestBody:
|
||||||
|
$ref: "#/components/requestBodies/gameEditRequest"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: "#/components/responses/gameResponse"
|
||||||
|
/engine/{uid}:
|
||||||
|
get:
|
||||||
|
operationId: gameEngine
|
||||||
|
parameters:
|
||||||
|
- name: uid
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: '#/components/responses/taskResponse'
|
||||||
|
/engine/{uid}/code:
|
||||||
|
post:
|
||||||
|
operationId: enterCode
|
||||||
|
parameters:
|
||||||
|
- name: uid
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
requestBody:
|
||||||
|
$ref: "#/components/requestBodies/enterCodeRequest"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: '#/components/responses/taskResponse'
|
||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
userTeam:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
role:
|
|
||||||
$ref: "#/components/schemas/userTeamRole"
|
|
||||||
required: [ id, name, role ]
|
|
||||||
userTeamRole:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- member
|
|
||||||
- captain
|
|
||||||
userRole:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- notVerified
|
|
||||||
- user
|
|
||||||
- creator
|
|
||||||
- admin
|
|
||||||
teamMember:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
user:
|
|
||||||
$ref: "#/components/schemas/userView"
|
|
||||||
role:
|
|
||||||
$ref: "#/components/schemas/userTeamRole"
|
|
||||||
createdAt:
|
|
||||||
type: string
|
|
||||||
required: [ user, role, createdAt ]
|
|
||||||
teamRequest:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
user:
|
|
||||||
$ref: "#/components/schemas/userView"
|
|
||||||
createdAt:
|
|
||||||
type: string
|
|
||||||
required: [ user, role, createdAt ]
|
|
||||||
userView:
|
userView:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -234,20 +98,6 @@ components:
|
||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
required: [ id, username ]
|
required: [ id, username ]
|
||||||
teamView:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
members:
|
|
||||||
type: integer
|
|
||||||
currentTeam:
|
|
||||||
type: boolean
|
|
||||||
createdAt:
|
|
||||||
type: string
|
|
||||||
required: [ id, name, createdAt ]
|
|
||||||
gameView:
|
gameView:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -257,28 +107,122 @@ components:
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
startAt:
|
type:
|
||||||
type: string
|
$ref: "#/components/schemas/gameType"
|
||||||
teams:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/teamView"
|
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- title
|
- title
|
||||||
- description
|
- description
|
||||||
- startAt
|
- type
|
||||||
- teams
|
taskView:
|
||||||
gameAdminListItem:
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
createdAt:
|
text:
|
||||||
type: string
|
type: string
|
||||||
required: [ id, title, createdAt ]
|
codes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/codeView'
|
||||||
|
entered:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/codeView'
|
||||||
|
solutions:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/solutionView'
|
||||||
|
required:
|
||||||
|
- title
|
||||||
|
- text
|
||||||
|
- codes
|
||||||
|
- entered
|
||||||
|
- solutions
|
||||||
|
codeView:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- description
|
||||||
|
solutionView:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
text:
|
||||||
|
type: string
|
||||||
|
after:
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- after
|
||||||
|
gameEdit:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
$ref: "#/components/schemas/gameType"
|
||||||
|
tasks:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/taskEdit"
|
||||||
|
points:
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- title
|
||||||
|
- description
|
||||||
|
- type
|
||||||
|
- tasks
|
||||||
|
- points
|
||||||
|
taskEdit:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
text:
|
||||||
|
type: string
|
||||||
|
codes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/codeEdit'
|
||||||
|
solutions:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/solutionEdit'
|
||||||
|
required:
|
||||||
|
- title
|
||||||
|
- text
|
||||||
|
- codes
|
||||||
|
- solutions
|
||||||
|
codeEdit:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- description
|
||||||
|
- code
|
||||||
|
solutionEdit:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
text:
|
||||||
|
type: string
|
||||||
|
after:
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- after
|
||||||
|
- text
|
||||||
|
gameType:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- virtual
|
||||||
|
- city
|
||||||
requestBodies:
|
requestBodies:
|
||||||
login:
|
login:
|
||||||
required: true
|
required: true
|
||||||
|
@ -308,6 +252,23 @@ components:
|
||||||
password2:
|
password2:
|
||||||
type: string
|
type: string
|
||||||
required: [ username, email, password, password2 ]
|
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: ''
|
||||||
|
@ -322,15 +283,21 @@ components:
|
||||||
type: string
|
type: string
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
team:
|
experience:
|
||||||
$ref: "#/components/schemas/userTeam"
|
type: integer
|
||||||
role:
|
level:
|
||||||
$ref: "#/components/schemas/userRole"
|
type: integer
|
||||||
|
games:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/gameView"
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- username
|
- username
|
||||||
- email
|
- email
|
||||||
- role
|
- experience
|
||||||
|
- level
|
||||||
|
- games
|
||||||
errorResponse:
|
errorResponse:
|
||||||
description: ''
|
description: ''
|
||||||
content:
|
content:
|
||||||
|
@ -343,36 +310,6 @@ components:
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
required: [ code, message ]
|
required: [ code, message ]
|
||||||
teamsListResponse:
|
|
||||||
description: ''
|
|
||||||
content:
|
|
||||||
'application/json':
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/teamView"
|
|
||||||
teamResponse:
|
|
||||||
description: ''
|
|
||||||
content:
|
|
||||||
'application/json':
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
members:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/teamMember"
|
|
||||||
requests:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/teamRequest"
|
|
||||||
createdAt:
|
|
||||||
type: string
|
|
||||||
required: [ id, name, members, requests, createdAt ]
|
|
||||||
gameListResponse:
|
gameListResponse:
|
||||||
description: ''
|
description: ''
|
||||||
content:
|
content:
|
||||||
|
@ -387,14 +324,12 @@ components:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/gameView"
|
$ref: "#/components/schemas/gameView"
|
||||||
gameAdminList:
|
taskResponse:
|
||||||
description: ''
|
description: ''
|
||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
type: array
|
$ref: "#/components/schemas/taskView"
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/gameAdminListItem"
|
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
cookieAuth:
|
cookieAuth:
|
||||||
type: apiKey
|
type: apiKey
|
||||||
|
|
246
api/server.go
246
api/server.go
|
@ -21,35 +21,17 @@ import (
|
||||||
// ServerInterface represents all server handlers.
|
// ServerInterface represents all server handlers.
|
||||||
type ServerInterface interface {
|
type ServerInterface interface {
|
||||||
|
|
||||||
// (GET /admin/games)
|
// (GET /engine/{uid})
|
||||||
GetAdminGames(ctx echo.Context) error
|
GameEngine(ctx echo.Context, uid int) error
|
||||||
|
|
||||||
|
// (POST /engine/{uid}/code)
|
||||||
|
EnterCode(ctx echo.Context, uid int) error
|
||||||
|
|
||||||
// (GET /games)
|
// (GET /games)
|
||||||
GetGames(ctx echo.Context) error
|
GetGames(ctx echo.Context) error
|
||||||
|
|
||||||
// (GET /teams)
|
// (POST /games)
|
||||||
GetTeams(ctx echo.Context) error
|
CreateGame(ctx echo.Context) error
|
||||||
|
|
||||||
// (POST /teams)
|
|
||||||
PostTeams(ctx echo.Context) error
|
|
||||||
|
|
||||||
// (DELETE /teams/{teamID})
|
|
||||||
DeleteTeamsTeamID(ctx echo.Context, teamID int) error
|
|
||||||
|
|
||||||
// (GET /teams/{teamID})
|
|
||||||
GetTeamsTeamID(ctx echo.Context, teamID int) error
|
|
||||||
|
|
||||||
// (DELETE /teams/{teamID}/members)
|
|
||||||
DeleteTeamsTeamIDMembers(ctx echo.Context, teamID int) error
|
|
||||||
|
|
||||||
// (POST /teams/{teamID}/members)
|
|
||||||
PostTeamsTeamIDMembers(ctx echo.Context, teamID int) error
|
|
||||||
|
|
||||||
// (POST /teams/{teamID}/requests)
|
|
||||||
PostTeamsTeamIDRequests(ctx echo.Context, teamID int) error
|
|
||||||
|
|
||||||
// (POST /teams/{teamID}/requests/{userID})
|
|
||||||
PostTeamsTeamIDRequestsUserID(ctx echo.Context, teamID int, userID int) error
|
|
||||||
|
|
||||||
// (GET /user)
|
// (GET /user)
|
||||||
GetUser(ctx echo.Context) error
|
GetUser(ctx echo.Context) error
|
||||||
|
@ -69,14 +51,39 @@ type ServerInterfaceWrapper struct {
|
||||||
Handler ServerInterface
|
Handler ServerInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAdminGames converts echo context to params.
|
// GameEngine converts echo context to params.
|
||||||
func (w *ServerInterfaceWrapper) GetAdminGames(ctx echo.Context) error {
|
func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
|
// ------------- Path parameter "uid" -------------
|
||||||
|
var uid int
|
||||||
|
|
||||||
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
|
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{})
|
||||||
|
|
||||||
// Invoke the callback with all the unmarshaled arguments
|
// Invoke the callback with all the unmarshaled arguments
|
||||||
err = w.Handler.GetAdminGames(ctx)
|
err = w.Handler.GameEngine(ctx, uid)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnterCode converts echo context to params.
|
||||||
|
func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error {
|
||||||
|
var err error
|
||||||
|
// ------------- Path parameter "uid" -------------
|
||||||
|
var uid int
|
||||||
|
|
||||||
|
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{})
|
||||||
|
|
||||||
|
// Invoke the callback with all the unmarshaled arguments
|
||||||
|
err = w.Handler.EnterCode(ctx, uid)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,141 +96,14 @@ func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTeams converts echo context to params.
|
// CreateGame converts echo context to params.
|
||||||
func (w *ServerInterfaceWrapper) GetTeams(ctx echo.Context) error {
|
func (w *ServerInterfaceWrapper) CreateGame(ctx echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
ctx.Set(CookieAuthScopes, []string{})
|
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.GetTeams(ctx)
|
err = w.Handler.CreateGame(ctx)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostTeams converts echo context to params.
|
|
||||||
func (w *ServerInterfaceWrapper) PostTeams(ctx echo.Context) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
ctx.Set(CookieAuthScopes, []string{})
|
|
||||||
|
|
||||||
// Invoke the callback with all the unmarshaled arguments
|
|
||||||
err = w.Handler.PostTeams(ctx)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTeamsTeamID converts echo context to params.
|
|
||||||
func (w *ServerInterfaceWrapper) DeleteTeamsTeamID(ctx echo.Context) error {
|
|
||||||
var err error
|
|
||||||
// ------------- Path parameter "teamID" -------------
|
|
||||||
var teamID int
|
|
||||||
|
|
||||||
err = runtime.BindStyledParameterWithLocation("simple", false, "teamID", runtime.ParamLocationPath, ctx.Param("teamID"), &teamID)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter teamID: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Set(CookieAuthScopes, []string{})
|
|
||||||
|
|
||||||
// Invoke the callback with all the unmarshaled arguments
|
|
||||||
err = w.Handler.DeleteTeamsTeamID(ctx, teamID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTeamsTeamID converts echo context to params.
|
|
||||||
func (w *ServerInterfaceWrapper) GetTeamsTeamID(ctx echo.Context) error {
|
|
||||||
var err error
|
|
||||||
// ------------- Path parameter "teamID" -------------
|
|
||||||
var teamID int
|
|
||||||
|
|
||||||
err = runtime.BindStyledParameterWithLocation("simple", false, "teamID", runtime.ParamLocationPath, ctx.Param("teamID"), &teamID)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter teamID: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Set(CookieAuthScopes, []string{})
|
|
||||||
|
|
||||||
// Invoke the callback with all the unmarshaled arguments
|
|
||||||
err = w.Handler.GetTeamsTeamID(ctx, teamID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTeamsTeamIDMembers converts echo context to params.
|
|
||||||
func (w *ServerInterfaceWrapper) DeleteTeamsTeamIDMembers(ctx echo.Context) error {
|
|
||||||
var err error
|
|
||||||
// ------------- Path parameter "teamID" -------------
|
|
||||||
var teamID int
|
|
||||||
|
|
||||||
err = runtime.BindStyledParameterWithLocation("simple", false, "teamID", runtime.ParamLocationPath, ctx.Param("teamID"), &teamID)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter teamID: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Set(CookieAuthScopes, []string{})
|
|
||||||
|
|
||||||
// Invoke the callback with all the unmarshaled arguments
|
|
||||||
err = w.Handler.DeleteTeamsTeamIDMembers(ctx, teamID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostTeamsTeamIDMembers converts echo context to params.
|
|
||||||
func (w *ServerInterfaceWrapper) PostTeamsTeamIDMembers(ctx echo.Context) error {
|
|
||||||
var err error
|
|
||||||
// ------------- Path parameter "teamID" -------------
|
|
||||||
var teamID int
|
|
||||||
|
|
||||||
err = runtime.BindStyledParameterWithLocation("simple", false, "teamID", runtime.ParamLocationPath, ctx.Param("teamID"), &teamID)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter teamID: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Set(CookieAuthScopes, []string{})
|
|
||||||
|
|
||||||
// Invoke the callback with all the unmarshaled arguments
|
|
||||||
err = w.Handler.PostTeamsTeamIDMembers(ctx, teamID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostTeamsTeamIDRequests converts echo context to params.
|
|
||||||
func (w *ServerInterfaceWrapper) PostTeamsTeamIDRequests(ctx echo.Context) error {
|
|
||||||
var err error
|
|
||||||
// ------------- Path parameter "teamID" -------------
|
|
||||||
var teamID int
|
|
||||||
|
|
||||||
err = runtime.BindStyledParameterWithLocation("simple", false, "teamID", runtime.ParamLocationPath, ctx.Param("teamID"), &teamID)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter teamID: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Set(CookieAuthScopes, []string{})
|
|
||||||
|
|
||||||
// Invoke the callback with all the unmarshaled arguments
|
|
||||||
err = w.Handler.PostTeamsTeamIDRequests(ctx, teamID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostTeamsTeamIDRequestsUserID converts echo context to params.
|
|
||||||
func (w *ServerInterfaceWrapper) PostTeamsTeamIDRequestsUserID(ctx echo.Context) error {
|
|
||||||
var err error
|
|
||||||
// ------------- Path parameter "teamID" -------------
|
|
||||||
var teamID int
|
|
||||||
|
|
||||||
err = runtime.BindStyledParameterWithLocation("simple", false, "teamID", runtime.ParamLocationPath, ctx.Param("teamID"), &teamID)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter teamID: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------- Path parameter "userID" -------------
|
|
||||||
var userID int
|
|
||||||
|
|
||||||
err = runtime.BindStyledParameterWithLocation("simple", false, "userID", runtime.ParamLocationPath, ctx.Param("userID"), &userID)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter userID: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Set(CookieAuthScopes, []string{})
|
|
||||||
|
|
||||||
// Invoke the callback with all the unmarshaled arguments
|
|
||||||
err = w.Handler.PostTeamsTeamIDRequestsUserID(ctx, teamID, userID)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,16 +173,10 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||||
Handler: si,
|
Handler: si,
|
||||||
}
|
}
|
||||||
|
|
||||||
router.GET(baseURL+"/admin/games", wrapper.GetAdminGames)
|
router.GET(baseURL+"/engine/:uid", wrapper.GameEngine)
|
||||||
|
router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode)
|
||||||
router.GET(baseURL+"/games", wrapper.GetGames)
|
router.GET(baseURL+"/games", wrapper.GetGames)
|
||||||
router.GET(baseURL+"/teams", wrapper.GetTeams)
|
router.POST(baseURL+"/games", wrapper.CreateGame)
|
||||||
router.POST(baseURL+"/teams", wrapper.PostTeams)
|
|
||||||
router.DELETE(baseURL+"/teams/:teamID", wrapper.DeleteTeamsTeamID)
|
|
||||||
router.GET(baseURL+"/teams/:teamID", wrapper.GetTeamsTeamID)
|
|
||||||
router.DELETE(baseURL+"/teams/:teamID/members", wrapper.DeleteTeamsTeamIDMembers)
|
|
||||||
router.POST(baseURL+"/teams/:teamID/members", wrapper.PostTeamsTeamIDMembers)
|
|
||||||
router.POST(baseURL+"/teams/:teamID/requests", wrapper.PostTeamsTeamIDRequests)
|
|
||||||
router.POST(baseURL+"/teams/:teamID/requests/:userID", wrapper.PostTeamsTeamIDRequestsUserID)
|
|
||||||
router.GET(baseURL+"/user", wrapper.GetUser)
|
router.GET(baseURL+"/user", wrapper.GetUser)
|
||||||
router.POST(baseURL+"/user/login", wrapper.PostUserLogin)
|
router.POST(baseURL+"/user/login", wrapper.PostUserLogin)
|
||||||
router.POST(baseURL+"/user/logout", wrapper.PostUserLogout)
|
router.POST(baseURL+"/user/logout", wrapper.PostUserLogout)
|
||||||
|
@ -313,24 +187,22 @@ 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/+RZwW7cNhD9lYLtUbA2iU+6uQgQGE2A1nVyMfZAS+M10xWpkpQDw9C/FzMitdKK0lLr",
|
"H4sIAAAAAAAC/8xYTY+kRgz9K5GTIxp6P07cktVqFGUPyWSTS6sPFfAwtQtVpFz0bmvEf49cfDQ0BU0T",
|
||||||
"jWM0J8vikHzzZuaRo31iuSorJUFaw7InpuHfGoz9XRUC6MVWbYTEh1xJC9LiI6+qrci5FUqmX42iYZPf",
|
"ZrWnGRXGfu/Zxq5+hljnhVaoLEH0DAb/LZHsLzqR6A5QWTTvdIIP9RM+i7WyqNy/oigyGQsrtQo/kVZ8",
|
||||||
"Q8nxqdKqAm3dfCi52OKDfayAZcxYLeSGNQmruDHflC4Cg01CQISGgmU3bo3ejHXiZ6jbr5Bb1gynWF0D",
|
"RvET5oL/K4wu0NjGVawT5L/2VCBEQNZIlUJVBS6qNJhAtK+tDkFrpf/5hLGFamhmTYlVAKnI8X0i7Rps",
|
||||||
"vdgIY0G/NPzd4NvgaG1AS17CYc87y2RMQn+XKELojamUNM43rZW+cm+ewVGuir4rQlrYgEZHSzCGbyL8",
|
"Pxl8hAh+DM8ChPVTClu/E2EznUr1P4TAXMjMo0QAhSD6ok1yXabaR++NhZIZTCVZNN8a/vnha+/TktAo",
|
||||||
"pCV29mF3CjC5FhViYhnD9Te8hIuiFPKjMHaRB8JCSeB/03DHMvZruquDtDUz6WD5Swsl7umAca354xwu",
|
"kS8okM4yGIvQj7JIEHdChVbUcDNGm4fmZLtal8piioaJ5kgk0qWNcLb300mQYiMLxgQRQNMTHyTZVSSk",
|
||||||
"nHMUudHQvgj4FovIAi9PEWoN3EJxYYN5LYqpNChvQZto5xDsJ5ozdi9hE5WTeO1ats9VOynA4zBDBRac",
|
"xZyWdMffEr9wtAaSMEac5hCtQrMMhD+oFfR586DsdC4ol+YGxTPdYPi1QCNRxRPFxarQBokMQCb+CBke",
|
||||||
"K0XvT2/LpEdNbPbi9ub7pglusSRNUG9OkCbTqjmVIlpt4ZAzBA7tHHcx9tdot0hyKdAB3SWEcaFtEscM",
|
"MfM/Wt7CkjvV08c9em2kltOy8q+CRtSuCd1nfOkounB4jUbfOJgaXPUDJ/M3gOGL302zUfz5MAEUWjZ7",
|
||||||
"kTFWkROVlRV2G+tQazufpa1uUbqMEA5cXIDRWK7thF+U/ydI5eOY6Hu0w+lRhdjpydLCCMZmN2arz3D8",
|
"wTjb3AvLS42t64k6LjUrbeaXoT64XsQf2e5SltptcJEl57JF3zGcku1jAwBVmbPPozS2FFyVsbSn3mtn",
|
||||||
"P2ZOy0ng3uBz9lDQ+yq40K8XxRhOzHmAea01SHvt1MKN3yq1BS4jj6zx4AIhcSIy710nbCidsi5xtlT2",
|
"zF1T3az2VMu9kD6uCWdE8mlCOivZxl9O4rFZLDwc8Ku9Xsm1g8Z6Lr5f4I3i+wJ39evt4eV90H2SPH3Q",
|
||||||
"C2hxJ8DrkV9G4RNHQektNoyJ93dI1ZSv0wfq4rSdoWBCP3eA9xloQ4Be88ryGW/DiTHl7bEnQUD6E2Yg",
|
"klvubZAOX2f5WU+X1EQLOT9BQ7UPdEqq6c/dbVJNTSd3K8FkE1/rZZ/y+EKyt6TnE8CMMC6NtKc/GW8r",
|
||||||
"r7Wwj38jG/42rP4RcFHbe0KBJ0L7ylORMQPGtLLjhawSf4A7lIW8U4St1TQm/6LKTNgDaNOeMG/OVmcr",
|
"vf4s8efSPjmWPC3rIwigntlASNRr/whEIX/DZquT6lE74DUDUH+4C1cARzRUT99Xd7u7HbPUBSpRSIjg",
|
||||||
"9EVVIHklWMbenb05W1FLYO8JRkoZQvfE9hQCqhBkiM7wy4Jl7ANYOpk+kNVee/B2tZoKfmc3vCIPKGHZ",
|
"zd2ru51byu2TgxGiSqXC8LmUScUHKTqluErcbvRrAhHc88Byhu5dI3K0aAiifQOd/Z2Bl+5LNlzsg95y",
|
||||||
"zZCMm3H6NmuccRjk8/ANrlh7EFsA3Uk0BeCaDI4BML7kNQk7X707PHPYnlFuVMoE8P2pTA+g798fn3F/",
|
"dfk1qA4XS//r3W4q5Z1dOFgoKyfKgE3YDvNCk4fT+/Ye/YKU2qv7aZpN73Yfjq721Va6dGuoP71o753B",
|
||||||
"i6uPqbIYNZrRRA05Oj+Coy6a6RP+uXzftLeXLVgYM/ee3hN312RNFaR5CZbE/8YVMFbVrnytNx3210mP",
|
"mnCj602/4CHaH9za4svAO4PC4n29cd4s1uVPDdVa8FPAhz26h5jhap6GIsmlgkN1cNry2jwn7V/kRujt",
|
||||||
"zn3ZadYjSs5H1yr2nNSYzdwXd+4HxjvtHd3Rcf/UdXT/O4YOqsYLMnAKbQp9TAj0RnO9vF/i1WtX2v+k",
|
"4AaXlyqAt7s3118a3peb9LOnsPuRxJ+N3zU5qB+c2YqE1P6rbZjubmd6UXV93rq0i4iz3Qj/29GqCFTG",
|
||||||
"ERXGq90HiZ+h1j0/6RPe05zaLyLqM837bnQlwZVqv+mPKCBeVVo9QKgf2qsTb/kK68R3nlPH3mfXXC6G",
|
"MRL90LjeGnH/J6F5zA+t5Yp8dVG+n5TNNuCBv6mE5th+pkuTQQQhT7bqUP0XAAD//zkXlqI5FQAA",
|
||||||
"OPgCdvR57CGm3c8n00mJUD+S2SjC4V17v9C49ZvTeLpa7mngMu39VrWNchztYm5Hps5zMOYXt/SpEfd/",
|
|
||||||
"LJrHfOUtj4hXt8vrCdlsz7ZG3TGgH7wu1nrLMpZi19msm/8CAAD//7N+kRk/HAAA",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|
167
api/types.go
167
api/types.go
|
@ -7,75 +7,71 @@ const (
|
||||||
CookieAuthScopes = "cookieAuth.Scopes"
|
CookieAuthScopes = "cookieAuth.Scopes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines values for UserRole.
|
// Defines values for GameType.
|
||||||
const (
|
const (
|
||||||
Admin UserRole = "admin"
|
City GameType = "city"
|
||||||
Creator UserRole = "creator"
|
Virtual GameType = "virtual"
|
||||||
NotVerified UserRole = "notVerified"
|
|
||||||
User UserRole = "user"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines values for UserTeamRole.
|
// CodeEdit defines model for codeEdit.
|
||||||
const (
|
type CodeEdit struct {
|
||||||
Captain UserTeamRole = "captain"
|
Code string `json:"code"`
|
||||||
Member UserTeamRole = "member"
|
Description string `json:"description"`
|
||||||
)
|
|
||||||
|
|
||||||
// GameAdminListItem defines model for gameAdminListItem.
|
|
||||||
type GameAdminListItem struct {
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
Id int `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CodeView defines model for codeView.
|
||||||
|
type CodeView struct {
|
||||||
|
Code *string `json:"code,omitempty"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GameEdit defines model for gameEdit.
|
||||||
|
type GameEdit struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
Points int `json:"points"`
|
||||||
|
Tasks []TaskEdit `json:"tasks"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Type GameType `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GameType defines model for gameType.
|
||||||
|
type GameType string
|
||||||
|
|
||||||
// GameView defines model for gameView.
|
// GameView defines model for gameView.
|
||||||
type GameView struct {
|
type GameView struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
StartAt string `json:"startAt"`
|
Title string `json:"title"`
|
||||||
Teams []TeamView `json:"teams"`
|
Type GameType `json:"type"`
|
||||||
Title string `json:"title"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamMember defines model for teamMember.
|
// SolutionEdit defines model for solutionEdit.
|
||||||
type TeamMember struct {
|
type SolutionEdit struct {
|
||||||
CreatedAt string `json:"createdAt"`
|
After int `json:"after"`
|
||||||
Role UserTeamRole `json:"role"`
|
Text string `json:"text"`
|
||||||
User UserView `json:"user"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamRequest defines model for teamRequest.
|
// SolutionView defines model for solutionView.
|
||||||
type TeamRequest struct {
|
type SolutionView struct {
|
||||||
CreatedAt string `json:"createdAt"`
|
After int `json:"after"`
|
||||||
User UserView `json:"user"`
|
Text *string `json:"text,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamView defines model for teamView.
|
// TaskEdit defines model for taskEdit.
|
||||||
type TeamView struct {
|
type TaskEdit struct {
|
||||||
CreatedAt string `json:"createdAt"`
|
Codes []CodeEdit `json:"codes"`
|
||||||
CurrentTeam *bool `json:"currentTeam,omitempty"`
|
Solutions []SolutionEdit `json:"solutions"`
|
||||||
Id int `json:"id"`
|
Text string `json:"text"`
|
||||||
Members *int `json:"members,omitempty"`
|
Title string `json:"title"`
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserRole defines model for userRole.
|
// TaskView defines model for taskView.
|
||||||
type UserRole string
|
type TaskView struct {
|
||||||
|
Codes []CodeView `json:"codes"`
|
||||||
// UserTeam defines model for userTeam.
|
Entered []CodeView `json:"entered"`
|
||||||
type UserTeam struct {
|
Solutions []SolutionView `json:"solutions"`
|
||||||
Id int `json:"id"`
|
Text string `json:"text"`
|
||||||
Name string `json:"name"`
|
Title string `json:"title"`
|
||||||
Role UserTeamRole `json:"role"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserTeamRole defines model for userTeamRole.
|
|
||||||
type UserTeamRole string
|
|
||||||
|
|
||||||
// UserView defines model for userView.
|
|
||||||
type UserView struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorResponse defines model for errorResponse.
|
// ErrorResponse defines model for errorResponse.
|
||||||
|
@ -84,33 +80,33 @@ type ErrorResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GameAdminList defines model for gameAdminList.
|
|
||||||
type GameAdminList = []GameAdminListItem
|
|
||||||
|
|
||||||
// GameListResponse defines model for gameListResponse.
|
// GameListResponse defines model for gameListResponse.
|
||||||
type GameListResponse = []GameView
|
type GameListResponse = []GameView
|
||||||
|
|
||||||
// TeamResponse defines model for teamResponse.
|
// GameResponse defines model for gameResponse.
|
||||||
type TeamResponse struct {
|
type GameResponse = GameView
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
Id int `json:"id"`
|
|
||||||
Members []TeamMember `json:"members"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Requests []TeamRequest `json:"requests"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TeamsListResponse defines model for teamsListResponse.
|
// TaskResponse defines model for taskResponse.
|
||||||
type TeamsListResponse = []TeamView
|
type TaskResponse = TaskView
|
||||||
|
|
||||||
// UserResponse defines model for userResponse.
|
// UserResponse defines model for userResponse.
|
||||||
type UserResponse struct {
|
type UserResponse struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Id int `json:"id"`
|
Experience int `json:"experience"`
|
||||||
Role UserRole `json:"role"`
|
Games []GameView `json:"games"`
|
||||||
Team *UserTeam `json:"team,omitempty"`
|
Id int `json:"id"`
|
||||||
Username string `json:"username"`
|
Level int `json:"level"`
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Login defines model for login.
|
||||||
type Login struct {
|
type Login struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
@ -125,19 +121,9 @@ type Register struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostTeamsJSONBody defines parameters for PostTeams.
|
// EnterCodeJSONBody defines parameters for EnterCode.
|
||||||
type PostTeamsJSONBody struct {
|
type EnterCodeJSONBody struct {
|
||||||
Name string `json:"name"`
|
Code string `json:"code"`
|
||||||
}
|
|
||||||
|
|
||||||
// PostTeamsTeamIDMembersJSONBody defines parameters for PostTeamsTeamIDMembers.
|
|
||||||
type PostTeamsTeamIDMembersJSONBody struct {
|
|
||||||
Members []int `json:"members"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostTeamsTeamIDRequestsUserIDJSONBody defines parameters for PostTeamsTeamIDRequestsUserID.
|
|
||||||
type PostTeamsTeamIDRequestsUserIDJSONBody struct {
|
|
||||||
Approve bool `json:"approve"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostUserLoginJSONBody defines parameters for PostUserLogin.
|
// PostUserLoginJSONBody defines parameters for PostUserLogin.
|
||||||
|
@ -154,14 +140,11 @@ type PostUserRegisterJSONBody struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostTeamsJSONRequestBody defines body for PostTeams for application/json ContentType.
|
// EnterCodeJSONRequestBody defines body for EnterCode for application/json ContentType.
|
||||||
type PostTeamsJSONRequestBody PostTeamsJSONBody
|
type EnterCodeJSONRequestBody EnterCodeJSONBody
|
||||||
|
|
||||||
// PostTeamsTeamIDMembersJSONRequestBody defines body for PostTeamsTeamIDMembers for application/json ContentType.
|
// CreateGameJSONRequestBody defines body for CreateGame for application/json ContentType.
|
||||||
type PostTeamsTeamIDMembersJSONRequestBody PostTeamsTeamIDMembersJSONBody
|
type CreateGameJSONRequestBody = GameEdit
|
||||||
|
|
||||||
// PostTeamsTeamIDRequestsUserIDJSONRequestBody defines body for PostTeamsTeamIDRequestsUserID for application/json ContentType.
|
|
||||||
type PostTeamsTeamIDRequestsUserIDJSONRequestBody PostTeamsTeamIDRequestsUserIDJSONBody
|
|
||||||
|
|
||||||
// PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType.
|
// PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType.
|
||||||
type PostUserLoginJSONRequestBody PostUserLoginJSONBody
|
type PostUserLoginJSONRequestBody PostUserLoginJSONBody
|
||||||
|
|
10
config.go
10
config.go
|
@ -7,11 +7,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
PgHost string `envconfig:"PG_HOST"`
|
PgHost string `envconfig:"POSTGRES_HOSTNAME"`
|
||||||
PgName string `envconfig:"PG_NAME"`
|
PgName string `envconfig:"POSTGRES_DB"`
|
||||||
PgUser string `envconfig:"PG_USER"`
|
PgUser string `envconfig:"POSTGRES_USER"`
|
||||||
PgPass string `envconfig:"PG_PASS"`
|
PgPass string `envconfig:"POSTGRES_PASSWORD"`
|
||||||
PgPort int `envconfig:"PG_PORT"`
|
PgPort int `envconfig:"POSTGRES_PORT"`
|
||||||
Listen string `envconfig:"LISTEN"`
|
Listen string `envconfig:"LISTEN"`
|
||||||
Secret string `envconfig:"SECRET"`
|
Secret string `envconfig:"SECRET"`
|
||||||
}
|
}
|
||||||
|
|
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
|
||||||
|
services:
|
||||||
|
# app:
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
# env_file:
|
||||||
|
# # Ensure that the variables in .env match the same variables in devcontainer.json
|
||||||
|
# - .env
|
||||||
|
|
||||||
|
# volumes:
|
||||||
|
# - ../..:/workspaces:cached
|
||||||
|
|
||||||
|
# # Overrides default command so things don't shut down after the process ends.
|
||||||
|
# command: sleep infinity
|
||||||
|
|
||||||
|
# # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||||
|
# network_mode: service:db
|
||||||
|
|
||||||
|
# # Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||||
|
# # (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine3.17
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
34
main.go
34
main.go
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -50,13 +49,8 @@ func main() {
|
||||||
|
|
||||||
if err := db.AutoMigrate(
|
if err := db.AutoMigrate(
|
||||||
&models.User{},
|
&models.User{},
|
||||||
&models.Team{},
|
|
||||||
&models.TeamMember{},
|
|
||||||
&models.TeamRequest{},
|
|
||||||
&models.Game{},
|
&models.Game{},
|
||||||
&models.GamePassing{},
|
&models.GameCursor{},
|
||||||
&models.Team{},
|
|
||||||
&models.TeamAtGame{},
|
|
||||||
&models.Task{},
|
&models.Task{},
|
||||||
&models.Solution{},
|
&models.Solution{},
|
||||||
&models.Code{},
|
&models.Code{},
|
||||||
|
@ -67,8 +61,8 @@ func main() {
|
||||||
|
|
||||||
// --[ Services ]--
|
// --[ Services ]--
|
||||||
userService := service.NewUser(db)
|
userService := service.NewUser(db)
|
||||||
teamService := service.NewTeam(db, userService)
|
|
||||||
gameService := service.NewGame(db)
|
gameService := service.NewGame(db)
|
||||||
|
engineService := service.NewEngine(db)
|
||||||
|
|
||||||
// --[ HTTP server ]--
|
// --[ HTTP server ]--
|
||||||
|
|
||||||
|
@ -108,14 +102,14 @@ 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{
|
// middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||||
TokenLookup: "cookie:_csrf",
|
// TokenLookup: "cookie:_csrf",
|
||||||
CookiePath: "/",
|
// CookiePath: "/",
|
||||||
// CookieDomain: "nquest.ru",
|
// // CookieDomain: "nquest.ru",
|
||||||
// CookieSecure: true,
|
// // CookieSecure: true,
|
||||||
CookieHTTPOnly: true,
|
// CookieHTTPOnly: true,
|
||||||
CookieSameSite: http.SameSiteStrictMode,
|
// CookieSameSite: http.SameSiteStrictMode,
|
||||||
}),
|
// }),
|
||||||
middleware.Gzip(),
|
middleware.Gzip(),
|
||||||
echoprometheus.NewMiddleware("nquest"),
|
echoprometheus.NewMiddleware("nquest"),
|
||||||
appmiddleware.User(userService),
|
appmiddleware.User(userService),
|
||||||
|
@ -129,12 +123,9 @@ func main() {
|
||||||
User: &controller.User{
|
User: &controller.User{
|
||||||
UserService: userService,
|
UserService: userService,
|
||||||
},
|
},
|
||||||
Team: &controller.Team{
|
|
||||||
UserService: userService,
|
|
||||||
TeamService: teamService,
|
|
||||||
},
|
|
||||||
Engine: &controller.Engine{
|
Engine: &controller.Engine{
|
||||||
GameService: gameService,
|
GameService: gameService,
|
||||||
|
EngineService: engineService,
|
||||||
},
|
},
|
||||||
Admin: &controller.Admin{
|
Admin: &controller.Admin{
|
||||||
GameService: gameService,
|
GameService: gameService,
|
||||||
|
@ -168,7 +159,6 @@ func main() {
|
||||||
type serverRouter struct {
|
type serverRouter struct {
|
||||||
*controller.Game
|
*controller.Game
|
||||||
*controller.User
|
*controller.User
|
||||||
*controller.Team
|
|
||||||
*controller.Engine
|
*controller.Engine
|
||||||
*controller.Admin
|
*controller.Admin
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"gitrepo.ru/neonxp/nquest/api"
|
"gitrepo.ru/neonxp/nquest/api"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
|
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
|
||||||
|
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/service"
|
"gitrepo.ru/neonxp/nquest/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,20 +14,63 @@ type Admin struct {
|
||||||
GameService *service.Game
|
GameService *service.Game
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Admin) GetAdminGames(ctx echo.Context) error {
|
// (POST /admin/games)
|
||||||
|
func (a *Admin) CreateGame(ctx echo.Context) error {
|
||||||
user := contextlib.GetUser(ctx)
|
user := contextlib.GetUser(ctx)
|
||||||
games, err := a.GameService.ListByAuthor(ctx.Request().Context(), user)
|
req := &api.GameEditRequest{}
|
||||||
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
result := make(api.GameAdminList, 0, len(games))
|
|
||||||
for _, g := range games {
|
return ctx.JSON(http.StatusCreated, api.GameResponse{
|
||||||
result = append(result, api.GameAdminListItem{
|
Id: int(game.ID),
|
||||||
Id: int(g.ID),
|
Title: game.Title,
|
||||||
Title: g.Title,
|
Description: game.Description,
|
||||||
CreatedAt: g.CreatedAt.Format("02.01.06 15:04"),
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
func (*Admin) mapCreateGameRequest(req *api.GameEditRequest, user *models.User) *models.Game {
|
||||||
|
game := &models.Game{
|
||||||
|
Visible: false,
|
||||||
|
Title: req.Title,
|
||||||
|
Description: req.Description,
|
||||||
|
Authors: []*models.User{
|
||||||
|
user,
|
||||||
|
},
|
||||||
|
Type: api.MapGameType(req.Type),
|
||||||
|
Tasks: make([]*models.Task, 0, len(req.Tasks)),
|
||||||
|
Points: req.Points,
|
||||||
|
}
|
||||||
|
for _, te := range req.Tasks {
|
||||||
|
task := &models.Task{
|
||||||
|
Title: te.Title,
|
||||||
|
Text: te.Text,
|
||||||
|
MaxTime: 0,
|
||||||
|
Solutions: make([]*models.Solution, 0, len(te.Solutions)),
|
||||||
|
Codes: make([]*models.Code, 0, len(te.Codes)),
|
||||||
|
}
|
||||||
|
for _, s := range te.Solutions {
|
||||||
|
task.Solutions = append(task.Solutions, &models.Solution{
|
||||||
|
After: s.After,
|
||||||
|
Text: s.Text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, ce := range te.Codes {
|
||||||
|
task.Codes = append(task.Codes, &models.Code{
|
||||||
|
Code: ce.Code,
|
||||||
|
Description: ce.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
game.Tasks = append(game.Tasks, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.JSON(http.StatusOK, result)
|
return game
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,54 +2,78 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"gitrepo.ru/neonxp/nquest/api"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
|
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/models"
|
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/service"
|
"gitrepo.ru/neonxp/nquest/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
GameService *service.Game
|
GameService *service.Game
|
||||||
|
EngineService *service.Engine
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *Engine) Get(c echo.Context) error {
|
// (GET /engine/{uid})
|
||||||
|
func (ec *Engine) GameEngine(c echo.Context, uid int) error {
|
||||||
user := contextlib.GetUser(c)
|
user := contextlib.GetUser(c)
|
||||||
team := user.Team.Team
|
|
||||||
gameID, err := strconv.Atoi(c.Param("ID"))
|
game, err := ec.GameService.GetByID(c.Request().Context(), uint(uid))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
game, err := ec.GameService.GetByID(c.Request().Context(), uint(gameID))
|
cursor, err := ec.EngineService.GetState(c.Request().Context(), game, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
state, err := ec.GameService.GetState(c.Request().Context(), game, team)
|
return c.JSON(http.StatusOK, mapCursorToTask(cursor))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
history, err := ec.GameService.GetHistory(c.Request().Context(), game, team)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Render(
|
|
||||||
http.StatusOK,
|
|
||||||
"engine_view",
|
|
||||||
&GetReponse{
|
|
||||||
Game: game,
|
|
||||||
State: state,
|
|
||||||
History: history,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetReponse struct {
|
// (POST /engine/{uid}/code)
|
||||||
Game *models.Game
|
func (ec *Engine) EnterCode(c echo.Context, uid int) error {
|
||||||
State *models.GamePassing
|
user := contextlib.GetUser(c)
|
||||||
History []*models.GamePassing
|
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
|
||||||
|
game, err := ec.GameService.GetByID(ctx, uint(uid))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &api.EnterCodeJSONRequestBody{}
|
||||||
|
if err := c.Bind(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor, err := ec.EngineService.EnterCode(ctx, game, user, req.Code)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, mapCursorToTask(cursor))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapCursorToTask(cursor *models.GameCursor) *api.TaskView {
|
||||||
|
resp := &api.TaskResponse{
|
||||||
|
Codes: make([]api.CodeView, 0, len(cursor.Task.Codes)),
|
||||||
|
Entered: make([]api.CodeView, 0, len(cursor.Codes)),
|
||||||
|
Solutions: []api.SolutionView{},
|
||||||
|
Text: cursor.Task.Text,
|
||||||
|
Title: cursor.Task.Title,
|
||||||
|
}
|
||||||
|
for _, code := range cursor.Task.Codes {
|
||||||
|
resp.Codes = append(resp.Codes, api.CodeView{
|
||||||
|
Description: code.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, code := range cursor.Codes {
|
||||||
|
resp.Entered = append(resp.Entered, api.CodeView{
|
||||||
|
Code: &code.Code,
|
||||||
|
Description: code.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return resp
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"gitrepo.ru/neonxp/nquest/api"
|
"gitrepo.ru/neonxp/nquest/api"
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
|
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/service"
|
"gitrepo.ru/neonxp/nquest/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,31 +19,12 @@ func (g *Game) GetGames(ctx echo.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
user := contextlib.GetUser(ctx)
|
|
||||||
userTeamID := uint(0)
|
|
||||||
if user != nil && user.Team != nil {
|
|
||||||
userTeamID = user.Team.TeamID
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := make(api.GameListResponse, 0, len(games))
|
resp := make(api.GameListResponse, 0, len(games))
|
||||||
for _, game := range games {
|
for _, game := range games {
|
||||||
teams := make([]api.TeamView, 0, len(game.Teams))
|
|
||||||
for _, tm := range game.Teams {
|
|
||||||
ct := tm.TeamID == userTeamID
|
|
||||||
teams = append(teams, api.TeamView{
|
|
||||||
CreatedAt: tm.CreatedAt.Format("02.01.06"),
|
|
||||||
CurrentTeam: &ct,
|
|
||||||
Id: int(tm.TeamID),
|
|
||||||
Members: nil,
|
|
||||||
Name: tm.Team.Name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
resp = append(resp, api.GameView{
|
resp = append(resp, api.GameView{
|
||||||
Id: int(game.ID),
|
Id: int(game.ID),
|
||||||
Title: game.Title,
|
Title: game.Title,
|
||||||
Description: game.Description,
|
Description: game.Description,
|
||||||
StartAt: game.StartAt.Format("02.01.06 15:04"),
|
|
||||||
Teams: teams,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,211 +0,0 @@
|
||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"gitrepo.ru/neonxp/nquest/api"
|
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
|
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/models"
|
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Team struct {
|
|
||||||
UserService *service.User
|
|
||||||
TeamService *service.Team
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Team) GetTeams(ctx echo.Context) error {
|
|
||||||
currentTeamID := uint(0)
|
|
||||||
user := contextlib.GetUser(ctx)
|
|
||||||
if user.Team != nil {
|
|
||||||
currentTeamID = user.Team.TeamID
|
|
||||||
}
|
|
||||||
teams, err := t.TeamService.List(ctx.Request().Context())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resp := make([]api.TeamView, 0, len(teams))
|
|
||||||
for _, t := range teams {
|
|
||||||
memberCount := len(t.Members)
|
|
||||||
isCurrentTeam := currentTeamID == t.ID
|
|
||||||
resp = append(resp, api.TeamView{
|
|
||||||
Id: int(t.ID),
|
|
||||||
Members: &memberCount,
|
|
||||||
Name: t.Name,
|
|
||||||
CurrentTeam: &isCurrentTeam,
|
|
||||||
CreatedAt: t.CreatedAt.Format("02.01.06"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.JSON(http.StatusOK, api.TeamsListResponse(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Team) GetTeamsTeamID(ctx echo.Context, teamID int) error {
|
|
||||||
return t.getTeamResponse(ctx, teamID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Team) PostTeamsTeamIDMembers(ctx echo.Context, teamID int) error {
|
|
||||||
user := contextlib.GetUser(ctx)
|
|
||||||
if user.Team == nil || user.Team.TeamID != uint(teamID) || user.Team.Role != models.Captain {
|
|
||||||
return ctx.JSON(http.StatusForbidden, api.ErrorResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
Message: "Вам нельзя менять состав команды",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &api.PostTeamsTeamIDMembersJSONRequestBody{}
|
|
||||||
if err := ctx.Bind(req); err != nil {
|
|
||||||
return ctx.JSON(http.StatusBadRequest, api.ErrorResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := t.TeamService.UpdateMembers(ctx.Request().Context(), uint(teamID), req.Members); err != nil {
|
|
||||||
return ctx.JSON(http.StatusBadRequest, api.ErrorResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.getTeamResponse(ctx, teamID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Team) PostTeams(ctx echo.Context) error {
|
|
||||||
req := &api.PostTeamsJSONRequestBody{}
|
|
||||||
if err := ctx.Bind(req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
user := contextlib.GetUser(ctx)
|
|
||||||
|
|
||||||
team, err := t.TeamService.Create(ctx.Request().Context(), req.Name, user)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.JSON(http.StatusCreated, api.TeamView{
|
|
||||||
Id: int(team.ID),
|
|
||||||
Name: team.Name,
|
|
||||||
CreatedAt: team.CreatedAt.Format("02.01.06"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Team) DeleteTeamsTeamID(ctx echo.Context, teamID int) error {
|
|
||||||
user := contextlib.GetUser(ctx)
|
|
||||||
if user.Team == nil || user.Team.TeamID != uint(teamID) || user.Team.Role != models.Captain {
|
|
||||||
return ctx.JSON(http.StatusForbidden, api.ErrorResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
Message: "Нельзя удалить не свою команду",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := t.TeamService.Delete(ctx.Request().Context(), uint(teamID)); err != nil {
|
|
||||||
return ctx.JSON(http.StatusForbidden, api.ErrorResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.NoContent(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Team) getTeamResponse(ctx echo.Context, teamID int) error {
|
|
||||||
team, err := t.TeamService.GetByID(ctx.Request().Context(), uint(teamID))
|
|
||||||
if err != nil {
|
|
||||||
return ctx.JSON(http.StatusNotFound, api.ErrorResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
members := make([]api.TeamMember, 0, len(team.Members))
|
|
||||||
for _, tm := range team.Members {
|
|
||||||
members = append(members, api.TeamMember{
|
|
||||||
Role: api.MapTeamRole[tm.Role],
|
|
||||||
User: api.UserView{
|
|
||||||
Id: int(tm.User.ID),
|
|
||||||
Username: tm.User.Username,
|
|
||||||
},
|
|
||||||
CreatedAt: tm.CreatedAt.Format("02.01.06"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
requests := make([]api.TeamRequest, 0, len(team.Requests))
|
|
||||||
for _, tm := range team.Requests {
|
|
||||||
requests = append(requests, api.TeamRequest{
|
|
||||||
User: api.UserView{
|
|
||||||
Id: int(tm.User.ID),
|
|
||||||
Username: tm.User.Username,
|
|
||||||
},
|
|
||||||
CreatedAt: tm.CreatedAt.Format("02.01.06"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.JSON(http.StatusOK, api.TeamResponse(api.TeamResponse{
|
|
||||||
Id: teamID,
|
|
||||||
Name: team.Name,
|
|
||||||
Members: members,
|
|
||||||
Requests: requests,
|
|
||||||
CreatedAt: team.CreatedAt.Format("02.01.06"),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// (POST /teams/{teamID}/requests)
|
|
||||||
func (t *Team) PostTeamsTeamIDRequests(ctx echo.Context, teamID int) error {
|
|
||||||
user := contextlib.GetUser(ctx)
|
|
||||||
if user.Team != nil {
|
|
||||||
return ctx.JSON(http.StatusForbidden, api.ErrorResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
Message: "Вы уже в другой команде",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := t.TeamService.Request(ctx.Request().Context(), uint(teamID), user); err != nil {
|
|
||||||
return ctx.JSON(http.StatusBadRequest, api.ErrorResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.getTeamResponse(ctx, teamID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// (POST /teams/{teamID}/requests/{userID})
|
|
||||||
func (t *Team) PostTeamsTeamIDRequestsUserID(ctx echo.Context, teamID int, userID int) error {
|
|
||||||
user := contextlib.GetUser(ctx)
|
|
||||||
if user.Team == nil || user.Team.TeamID != uint(teamID) || user.Team.Role != models.Captain {
|
|
||||||
return ctx.JSON(http.StatusForbidden, api.ErrorResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
Message: "Вам нельзя менять состав команды",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
req := &api.PostTeamsTeamIDRequestsUserIDJSONRequestBody{}
|
|
||||||
if err := ctx.Bind(req); err != nil {
|
|
||||||
return ctx.JSON(http.StatusBadRequest, api.ErrorResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := t.TeamService.ApproveRequest(ctx.Request().Context(), teamID, uint(userID), req.Approve); err != nil {
|
|
||||||
return ctx.JSON(http.StatusBadRequest, api.ErrorResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.getTeamResponse(ctx, teamID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// (DELETE /teams/{teamID}/members)
|
|
||||||
func (t *Team) DeleteTeamsTeamIDMembers(ctx echo.Context, teamID int) error {
|
|
||||||
user := contextlib.GetUser(ctx)
|
|
||||||
if err := t.TeamService.DeleteMember(ctx.Request().Context(), teamID, user.ID); err != nil {
|
|
||||||
return ctx.JSON(http.StatusBadRequest, api.ErrorResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.getTeamResponse(ctx, teamID)
|
|
||||||
}
|
|
|
@ -34,12 +34,10 @@ func (u *User) PostUserLogin(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := setUser(c, user); err != nil {
|
if err := setUser(c, user); err != nil {
|
||||||
if err != nil {
|
return c.JSON(http.StatusBadRequest, &api.ErrorResponse{
|
||||||
return c.JSON(http.StatusBadRequest, &api.ErrorResponse{
|
Code: http.StatusBadRequest,
|
||||||
Code: http.StatusBadRequest,
|
Message: err.Error(),
|
||||||
Message: err.Error(),
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapUser(c, user)
|
return mapUser(c, user)
|
||||||
|
@ -95,16 +93,6 @@ func (u *User) GetUser(c echo.Context) error {
|
||||||
return mapUser(c, user)
|
return mapUser(c, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapUser(c echo.Context, user *models.User) error {
|
|
||||||
return c.JSON(http.StatusOK, &api.UserResponse{
|
|
||||||
Id: int(user.ID),
|
|
||||||
Username: user.Username,
|
|
||||||
Email: user.Email,
|
|
||||||
Team: api.MapUserTeam(user.Team),
|
|
||||||
Role: api.MapUserRole[user.Role],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func setUser(c echo.Context, user *models.User) error {
|
func setUser(c echo.Context, user *models.User) error {
|
||||||
sess, err := session.Get("session", c)
|
sess, err := session.Get("session", c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -136,3 +124,26 @@ func setUser(c echo.Context, user *models.User) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapUser(c echo.Context, user *models.User) error {
|
||||||
|
games := make([]api.GameView, 0)
|
||||||
|
for _, gc := range user.Games {
|
||||||
|
if gc.Status == models.TaskFinished && gc.Task.Next == nil {
|
||||||
|
games = append(games, api.GameView{
|
||||||
|
Id: int(gc.GameID),
|
||||||
|
Title: gc.Game.Title,
|
||||||
|
Description: gc.Game.Description,
|
||||||
|
Type: api.MapGameTypeReverse(gc.Game.Type),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, &api.UserResponse{
|
||||||
|
Id: int(user.ID),
|
||||||
|
Username: user.Username,
|
||||||
|
Email: user.Email,
|
||||||
|
Experience: user.Experience,
|
||||||
|
Level: user.Experience / 1000,
|
||||||
|
Games: games,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
24
pkg/models/cursor.go
Normal file
24
pkg/models/cursor.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type GameCursor struct {
|
||||||
|
User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
||||||
|
UserID uint `gorm:"primaryKey"`
|
||||||
|
Game *Game `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
||||||
|
GameID uint `gorm:"primaryKey"`
|
||||||
|
Task *Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
||||||
|
TaskID uint `gorm:"primaryKey"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
FinishedAt *time.Time
|
||||||
|
Status CursorStatus
|
||||||
|
Codes []*Code `gorm:"many2many:passing_codes;"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CursorStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskStarted CursorStatus = iota
|
||||||
|
TaskFinished
|
||||||
|
TaskCanceled
|
||||||
|
)
|
|
@ -1,55 +1,20 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
Model
|
Model
|
||||||
|
|
||||||
Visible bool `gorm:"index"`
|
Visible bool `gorm:"index"`
|
||||||
Title string
|
Title string
|
||||||
Description string
|
Description string
|
||||||
StartAt time.Time
|
Tasks []*Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
Teams []*TeamAtGame `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
|
||||||
Tasks []*Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
|
||||||
FirstTask *Task `gorm:"foreignKey:ID"`
|
|
||||||
FirstTaskID uint
|
|
||||||
Authors []*User `gorm:"many2many:game_authors"`
|
Authors []*User `gorm:"many2many:game_authors"`
|
||||||
|
Type GameType
|
||||||
|
Points int
|
||||||
}
|
}
|
||||||
|
|
||||||
type TeamAtGame struct {
|
type GameType int
|
||||||
Team *Team `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
|
||||||
TeamID uint `gorm:"primaryKey"`
|
|
||||||
Game *Game `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
|
||||||
GameID uint `gorm:"primaryKey"`
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type GamePassing struct {
|
|
||||||
Team *Team `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
|
||||||
TeamID uint `gorm:"primaryKey"`
|
|
||||||
Game *Game `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
|
||||||
GameID uint `gorm:"primaryKey"`
|
|
||||||
Task *Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
|
||||||
TaskID uint `gorm:"primaryKey"`
|
|
||||||
CreatedAt time.Time
|
|
||||||
FinishedAt *time.Time
|
|
||||||
Deadline time.Time
|
|
||||||
Status Passing
|
|
||||||
Codes []*Code `gorm:"many2many:passing_codes;"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GamePassing) Timeouted() bool {
|
|
||||||
return g.Deadline.Before(time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
type Passing int
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PassStarted Passing = iota
|
VirtualGame GameType = iota
|
||||||
PassFinished
|
CityGame
|
||||||
PassCanceled
|
|
||||||
PassFailed
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,7 +6,6 @@ type Task struct {
|
||||||
Title string
|
Title string
|
||||||
Text string
|
Text string
|
||||||
MaxTime int
|
MaxTime int
|
||||||
Game *Game
|
|
||||||
GameID uint
|
GameID uint
|
||||||
Solutions []*Solution `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
Solutions []*Solution `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Team struct {
|
|
||||||
Model
|
|
||||||
Name string `gorm:"unique,not_null" json:"name"`
|
|
||||||
Members []*TeamMember `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"members"`
|
|
||||||
Requests []*TeamRequest `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
|
|
||||||
Games []*TeamAtGame `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TeamMember struct {
|
|
||||||
Team *Team `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"team"`
|
|
||||||
TeamID uint `gorm:"primaryKey" json:"-"`
|
|
||||||
User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"-"`
|
|
||||||
UserID uint `gorm:"primaryKey" json:"-"`
|
|
||||||
Role Role `json:"role"`
|
|
||||||
CreatedAt time.Time `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TeamRequest struct {
|
|
||||||
Team *Team `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
|
||||||
TeamID uint `gorm:"primaryKey"`
|
|
||||||
User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
|
|
||||||
UserID uint `gorm:"primaryKey"`
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type Role int
|
|
||||||
|
|
||||||
const (
|
|
||||||
Captain Role = iota
|
|
||||||
Member
|
|
||||||
)
|
|
|
@ -4,24 +4,13 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var ErrEmptyPassword = errors.New("empty password")
|
||||||
ErrEmptyPassword = errors.New("empty password")
|
|
||||||
)
|
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Model
|
Model
|
||||||
Username string `gorm:"unique" json:"username"`
|
Username string `gorm:"unique" json:"username"`
|
||||||
Email string `gorm:"unique" json:"email"`
|
Email string `gorm:"unique" json:"email"`
|
||||||
Password string `json:"-"`
|
Password string `json:"-"`
|
||||||
Team *TeamMember `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"team"`
|
Experience int
|
||||||
Role UserRole `json:"role"`
|
Games []*GameCursor
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRole int
|
|
||||||
|
|
||||||
const (
|
|
||||||
RoleNotVerified UserRole = iota
|
|
||||||
RoleUser
|
|
||||||
RoleCreator
|
|
||||||
RoleAdmin
|
|
||||||
)
|
|
||||||
|
|
123
pkg/service/engine.go
Normal file
123
pkg/service/engine.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrGameNotStarted = errors.New("game not started")
|
||||||
|
ErrInvalidCode = errors.New("invalid code")
|
||||||
|
ErrOldCode = errors.New("old code")
|
||||||
|
ErrGameFinished = errors.New("game finished")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Engine struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEngine(db *gorm.DB) *Engine {
|
||||||
|
return &Engine{
|
||||||
|
DB: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) GetState(ctx context.Context, game *models.Game, user *models.User) (*models.GameCursor, error) {
|
||||||
|
db := e.DB.WithContext(ctx)
|
||||||
|
|
||||||
|
// Пытаемся получить GamePassing
|
||||||
|
cursor := &models.GameCursor{
|
||||||
|
User: user,
|
||||||
|
Game: game,
|
||||||
|
Task: game.Tasks[0],
|
||||||
|
Status: models.TaskStarted,
|
||||||
|
Codes: []*models.Code{},
|
||||||
|
}
|
||||||
|
err := db.
|
||||||
|
Where(`user_id = ? and game_id = ? and status = ?`, user.ID, game.ID, models.TaskStarted).
|
||||||
|
Preload("Task").
|
||||||
|
Preload("Task.Codes").
|
||||||
|
Preload("Task.Next").
|
||||||
|
Preload("Task.Next.Codes").
|
||||||
|
Preload("Codes").
|
||||||
|
FirstOrCreate(cursor).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
if err, ok := err.(*pgconn.PgError); ok {
|
||||||
|
if err.Code == "23505" {
|
||||||
|
return nil, ErrGameNotStarted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) EnterCode(ctx context.Context, game *models.Game, user *models.User, code string) (*models.GameCursor, error) {
|
||||||
|
db := e.DB.WithContext(ctx)
|
||||||
|
st, err := e.GetState(ctx, game, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
code = strings.Trim(code, " \n\t")
|
||||||
|
code = strings.ToLower(code)
|
||||||
|
var currentCode *models.Code
|
||||||
|
for _, c := range st.Task.Codes {
|
||||||
|
if c.Code == code {
|
||||||
|
currentCode = c
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if currentCode == nil {
|
||||||
|
return nil, ErrInvalidCode
|
||||||
|
}
|
||||||
|
for _, c := range st.Codes {
|
||||||
|
if c.ID == currentCode.ID {
|
||||||
|
return nil, ErrOldCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
st.Codes = append(st.Codes, currentCode)
|
||||||
|
|
||||||
|
if err := db.Save(st).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(st.Codes) != len(st.Task.Codes) {
|
||||||
|
return st, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Уровень пройден. Выдаем следующий
|
||||||
|
|
||||||
|
st.Status = models.TaskFinished
|
||||||
|
if err := db.Save(st).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if st.Task.Next == nil {
|
||||||
|
|
||||||
|
user.Experience += st.Game.Points
|
||||||
|
if err := db.Save(user).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrGameFinished
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := &models.GameCursor{
|
||||||
|
User: user,
|
||||||
|
Game: game,
|
||||||
|
Task: st.Task.Next,
|
||||||
|
Status: models.TaskStarted,
|
||||||
|
Codes: []*models.Code{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState, db.Create(newState).Error
|
||||||
|
}
|
|
@ -2,18 +2,11 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/models"
|
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ErrGameNotStarted = errors.New("game not started")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
DB *gorm.DB
|
DB *gorm.DB
|
||||||
}
|
}
|
||||||
|
@ -30,7 +23,7 @@ func (gs *Game) GetByID(ctx context.Context, id uint) (*models.Game, error) {
|
||||||
|
|
||||||
return g, gs.DB.
|
return g, gs.DB.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Preload("FirstTask").
|
Preload("Tasks").
|
||||||
First(g, id).
|
First(g, id).
|
||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
@ -40,9 +33,7 @@ func (gs *Game) List(ctx context.Context) ([]*models.Game, error) {
|
||||||
|
|
||||||
return games, gs.DB.
|
return games, gs.DB.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Preload("Teams").
|
Order("created_at DESC").
|
||||||
Preload("Teams.Team").
|
|
||||||
Order("start_at DESC").
|
|
||||||
Find(&games, "visible = true").
|
Find(&games, "visible = true").
|
||||||
Limit(20).
|
Limit(20).
|
||||||
Error
|
Error
|
||||||
|
@ -54,93 +45,33 @@ func (gs *Game) GetTaskID(ctx context.Context, id uint) (*models.Task, error) {
|
||||||
return t, gs.DB.WithContext(ctx).Preload("Next").First(t, id).Error
|
return t, gs.DB.WithContext(ctx).Preload("Next").First(t, id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *Game) GetHistory(ctx context.Context, game *models.Game, team *models.Team) ([]*models.GamePassing, error) {
|
|
||||||
db := gs.DB.WithContext(ctx)
|
|
||||||
history := []*models.GamePassing{}
|
|
||||||
|
|
||||||
return history, db.Where(`team_id = ? and game_id = ?`, team.ID, game.ID).Find(&history).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gs *Game) GetState(ctx context.Context, game *models.Game, team *models.Team) (*models.GamePassing, error) {
|
|
||||||
db := gs.DB.WithContext(ctx)
|
|
||||||
|
|
||||||
if !game.StartAt.Before(time.Now()) {
|
|
||||||
return nil, ErrGameNotStarted
|
|
||||||
}
|
|
||||||
|
|
||||||
// Пытаемся получить GamePassing
|
|
||||||
gamepass := &models.GamePassing{
|
|
||||||
Team: team,
|
|
||||||
Game: game,
|
|
||||||
Task: game.FirstTask,
|
|
||||||
Status: models.PassStarted,
|
|
||||||
Codes: []*models.Code{},
|
|
||||||
CreatedAt: game.StartAt,
|
|
||||||
Deadline: game.StartAt.Add(time.Minute * time.Duration(game.FirstTask.MaxTime)),
|
|
||||||
}
|
|
||||||
err := db.
|
|
||||||
Where(`team_id = ? and game_id = ? and status = ?`, team.ID, game.ID, models.PassStarted).
|
|
||||||
Preload("Task").
|
|
||||||
FirstOrCreate(gamepass).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
if err, ok := err.(*pgconn.PgError); ok {
|
|
||||||
if err.Code == "23505" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
if !gamepass.Timeouted() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
gamepass.Status = models.PassFailed
|
|
||||||
gamepass.FinishedAt = &gamepass.Deadline
|
|
||||||
if err := db.Save(gamepass).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
taskID := gamepass.TaskID
|
|
||||||
task, err := gs.GetTaskID(ctx, taskID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if task.Next == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gamepass = &models.GamePassing{
|
|
||||||
Team: team,
|
|
||||||
Game: game,
|
|
||||||
Task: task.Next,
|
|
||||||
CreatedAt: gamepass.Deadline,
|
|
||||||
Deadline: gamepass.Deadline.Add(time.Minute * time.Duration(task.Next.MaxTime)),
|
|
||||||
Status: models.PassStarted,
|
|
||||||
Codes: []*models.Code{},
|
|
||||||
}
|
|
||||||
if err := db.Create(gamepass).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gamepass, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*models.Game, error) {
|
func (gs *Game) ListByAuthor(ctx context.Context, author *models.User) ([]*models.Game, error) {
|
||||||
games := make([]*models.Game, 0)
|
games := make([]*models.Game, 0)
|
||||||
|
|
||||||
return games, gs.DB.
|
return games, gs.DB.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Model(&models.Game{}).
|
Model(&models.Game{}).
|
||||||
Preload("Teams").
|
|
||||||
Preload("Teams.Team").
|
|
||||||
Preload("Authors", gs.DB.Where("id = ?", author.ID)).
|
Preload("Authors", gs.DB.Where("id = ?", author.ID)).
|
||||||
Order("created_at DESC").
|
Order("created_at DESC").
|
||||||
Find(&games).
|
Find(&games).
|
||||||
Limit(20).
|
Limit(20).
|
||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gs *Game) CreateGame(ctx context.Context, game *models.Game) (*models.Game, error) {
|
||||||
|
return game, gs.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Create(game).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, t := range game.Tasks {
|
||||||
|
if i < len(game.Tasks)-1 {
|
||||||
|
t.Next = game.Tasks[i+1]
|
||||||
|
if err := tx.Save(t).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1,209 +0,0 @@
|
||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitrepo.ru/neonxp/nquest/pkg/models"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrTeamNotFound = errors.New("team not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Team struct {
|
|
||||||
DB *gorm.DB
|
|
||||||
User *User
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTeam returns new Team.
|
|
||||||
func NewTeam(db *gorm.DB, user *User) *Team {
|
|
||||||
return &Team{
|
|
||||||
DB: db,
|
|
||||||
User: user,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Team) GetByID(ctx context.Context, id uint) (*models.Team, error) {
|
|
||||||
t := new(models.Team)
|
|
||||||
|
|
||||||
err := ts.DB.
|
|
||||||
WithContext(ctx).
|
|
||||||
Preload("Members").
|
|
||||||
Preload("Members.User").
|
|
||||||
Preload("Requests").
|
|
||||||
Preload("Requests.User").
|
|
||||||
First(t, id).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, ErrTeamNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Team) List(ctx context.Context) ([]*models.Team, error) {
|
|
||||||
teams := []*models.Team{}
|
|
||||||
|
|
||||||
return teams, ts.DB.WithContext(ctx).Preload("Members").Find(&teams).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Team) Create(ctx context.Context, name string, user *models.User) (*models.Team, error) {
|
|
||||||
t := &models.Team{
|
|
||||||
Name: name,
|
|
||||||
Members: []*models.TeamMember{{
|
|
||||||
User: user,
|
|
||||||
Role: models.Captain,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
|
|
||||||
db := ts.DB.WithContext(ctx)
|
|
||||||
|
|
||||||
if err := db.Delete(&models.TeamRequest{}, `user_id = ?`, user.ID).Error; err != nil {
|
|
||||||
return t, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return t, db.Create(t).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Team) Delete(ctx context.Context, id uint) error {
|
|
||||||
if err := ts.DB.WithContext(ctx).Delete(&models.Team{}, id).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return ErrTeamNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Team) Request(ctx context.Context, teamID uint, user *models.User) error {
|
|
||||||
team, err := ts.GetByID(ctx, teamID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ts.DB.
|
|
||||||
WithContext(ctx).
|
|
||||||
Clauses(clause.OnConflict{DoNothing: true}).
|
|
||||||
Create(&models.TeamRequest{
|
|
||||||
Team: team,
|
|
||||||
User: user,
|
|
||||||
}).
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Team) UpdateMembers(
|
|
||||||
ctx context.Context,
|
|
||||||
teamID uint,
|
|
||||||
newMembers []int,
|
|
||||||
) error {
|
|
||||||
team, err := ts.GetByID(ctx, teamID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
newMembersList := make([]*models.TeamMember, 0, len(newMembers))
|
|
||||||
|
|
||||||
for _, tm := range team.Members {
|
|
||||||
idx, ok := slices.BinarySearch(newMembers, int(tm.UserID))
|
|
||||||
if ok {
|
|
||||||
newMembers = slices.Delete(newMembers, idx, idx)
|
|
||||||
newMembersList = append(newMembersList, tm)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := ts.DB.WithContext(ctx).Delete(tm).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, userID := range newMembers {
|
|
||||||
_, found := slices.BinarySearchFunc(team.Members, userID, func(tm *models.TeamMember, i int) int {
|
|
||||||
return int(tm.UserID) - i
|
|
||||||
})
|
|
||||||
if found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
user, err := ts.User.GetUserByID(ctx, uint(userID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
newMembersList = append(newMembersList, &models.TeamMember{
|
|
||||||
TeamID: teamID,
|
|
||||||
Team: team,
|
|
||||||
UserID: user.ID,
|
|
||||||
User: user,
|
|
||||||
Role: models.Member,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
team.Members = newMembersList
|
|
||||||
|
|
||||||
return ts.DB.
|
|
||||||
WithContext(ctx).
|
|
||||||
Session(&gorm.Session{FullSaveAssociations: true}).
|
|
||||||
Updates(team).
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Team) ApproveRequest(ctx context.Context, teamID int, userID uint, approve bool) error {
|
|
||||||
team, err := ts.GetByID(ctx, uint(teamID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
idx, found := slices.BinarySearchFunc(team.Requests, userID, func(tr *models.TeamRequest, i uint) int {
|
|
||||||
return int(tr.UserID - i)
|
|
||||||
})
|
|
||||||
if !found {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
request := team.Requests[idx]
|
|
||||||
team.Requests = slices.DeleteFunc(team.Requests, func(tr *models.TeamRequest) bool {
|
|
||||||
return tr.UserID == uint(userID)
|
|
||||||
})
|
|
||||||
if err := ts.DB.WithContext(ctx).Delete(request).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if approve {
|
|
||||||
team.Members = append(team.Members, &models.TeamMember{
|
|
||||||
Team: team,
|
|
||||||
TeamID: uint(teamID),
|
|
||||||
UserID: uint(userID),
|
|
||||||
Role: models.Member,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return ts.DB.WithContext(ctx).Save(team).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Team) DeleteMember(ctx context.Context, teamID int, userID uint) error {
|
|
||||||
team, err := ts.GetByID(ctx, uint(teamID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
idx, found := slices.BinarySearchFunc(team.Members, userID, func(tm *models.TeamMember, u uint) int {
|
|
||||||
return int(tm.UserID - u)
|
|
||||||
})
|
|
||||||
if !found {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
member := team.Members[idx]
|
|
||||||
|
|
||||||
return ts.DB.WithContext(ctx).Delete(member).Error
|
|
||||||
}
|
|
|
@ -61,7 +61,6 @@ func (s *User) Register(ctx context.Context, username, email, password, password
|
||||||
Username: username,
|
Username: username,
|
||||||
Email: normalizer.NewNormalizer().Normalize(email),
|
Email: normalizer.NewNormalizer().Normalize(email),
|
||||||
Password: hex.EncodeToString(hashed),
|
Password: hex.EncodeToString(hashed),
|
||||||
Role: models.RoleUser,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.DB.WithContext(ctx).Create(u).Error
|
err = s.DB.WithContext(ctx).Create(u).Error
|
||||||
|
@ -83,7 +82,6 @@ func (s *User) Login(ctx context.Context, email, password string) (*models.User,
|
||||||
err := s.DB.
|
err := s.DB.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Where("email = ?", nemail).
|
Where("email = ?", nemail).
|
||||||
Preload("Team").Preload("Team.Team").
|
|
||||||
First(u).
|
First(u).
|
||||||
Error
|
Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -104,7 +102,12 @@ func (s *User) Login(ctx context.Context, email, password string) (*models.User,
|
||||||
func (s *User) GetUserByID(ctx context.Context, userID uint) (*models.User, error) {
|
func (s *User) GetUserByID(ctx context.Context, userID uint) (*models.User, error) {
|
||||||
u := new(models.User)
|
u := new(models.User)
|
||||||
|
|
||||||
return u, s.DB.WithContext(ctx).Preload("Team").Preload("Team.Team").First(u, userID).Error
|
return u, s.DB.WithContext(ctx).
|
||||||
|
Preload("Games").
|
||||||
|
Preload("Games.Game").
|
||||||
|
Preload("Games.Task").
|
||||||
|
Preload("Games.Task.Next").
|
||||||
|
First(u, userID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *User) GetUser(c echo.Context) *models.User {
|
func (s *User) GetUser(c echo.Context) *models.User {
|
||||||
|
|
132
requests.http
Normal file
132
requests.http
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
POST http://localhost:8000/api/user/register
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"username": "neonxp",
|
||||||
|
"email": "i@neonxp.ru",
|
||||||
|
"password": "password",
|
||||||
|
"password2": "password"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:8000/api/user
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:8000/api/games
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:8000/api/user/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "i@neonxp.ru",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:8000/api/games
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Тестовая игра",
|
||||||
|
"description": "Описание тестовой игры",
|
||||||
|
"type": "city",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"title": "Задание 1",
|
||||||
|
"text": "Текст первого задания",
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"description": "1+",
|
||||||
|
"code": "nq1111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Задание 2",
|
||||||
|
"text": "Текст второго задания",
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"description": "1+",
|
||||||
|
"code": "nq2211"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "2+",
|
||||||
|
"code": "nq2222"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [
|
||||||
|
{
|
||||||
|
"text": "Помощь 1",
|
||||||
|
"after": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Помощь 2",
|
||||||
|
"after": 60
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Задание 3",
|
||||||
|
"text": "Текст третьего задания",
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"description": "1+",
|
||||||
|
"code": "nq3311"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "2+",
|
||||||
|
"code": "nq3322"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "3+",
|
||||||
|
"code": "nq3333"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:8000/api/engine/1
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:8000/api/engine/1/code
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": "NQ1111"
|
||||||
|
}
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:8000/api/engine/1/code
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": "NQ2211"
|
||||||
|
}
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:8000/api/engine/1/code
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": "NQ2222"
|
||||||
|
}
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:8000/api/engine/1/code
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": "NQ3322"
|
||||||
|
}
|
|
@ -1,35 +0,0 @@
|
||||||
{{ template "header" . }}
|
|
||||||
<h1>Команды</h1>
|
|
||||||
{{$MyTeam := 0}}
|
|
||||||
{{if and .User .User.Team }}
|
|
||||||
{{$MyTeam = .User.Team.Team.ID}}
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if and .User (not .User.Team) }}
|
|
||||||
<div class="d-grid gap-2 col-6 mx-auto mb-2">
|
|
||||||
<a href="/team/new" class="btn btn-primary">Создать команду</a>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead class="table-primary">
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Название</th>
|
|
||||||
<th scope="col" class="thin">Количество участников</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{range .Teams}}
|
|
||||||
<tr class="{{ if eq $MyTeam .ID }}active{{ end }}">
|
|
||||||
<td>
|
|
||||||
<a href="/team/{{.ID}}">
|
|
||||||
{{.Name}}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{len .Members}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{{ template "footer" . }}
|
|
|
@ -1,130 +0,0 @@
|
||||||
{{ template "header" . }}
|
|
||||||
|
|
||||||
{{ $IsAdmin := false }}
|
|
||||||
{{ if and .User.Team (eq .Team.ID .User.Team.TeamID) (eq .User.Team.Role 0) }}
|
|
||||||
{{ $IsAdmin = true }}
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<h1>{{ .Team.Name }}</h1>
|
|
||||||
<p class="fw-light">Создана {{ .Team.CreatedAt.Format "02.01.2006" }}</p>
|
|
||||||
|
|
||||||
{{ if $IsAdmin }}
|
|
||||||
<p>Вы капитан команды</p>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ if or (not .User.Team) (ne .Team.ID .User.Team.TeamID) }}
|
|
||||||
{{ $requested := false }}
|
|
||||||
{{ $userID := .User.ID }}
|
|
||||||
{{ range .Team.Requests }}
|
|
||||||
{{ if eq .User.ID $userID }}
|
|
||||||
{{ $requested = true }}
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ if $requested }}
|
|
||||||
<div class="alert alert-secondary">Заявка в команду рассматривается.</div>
|
|
||||||
{{ else }}
|
|
||||||
<form method="post" class="mt-2" action="/team/{{.Team.ID}}/request">
|
|
||||||
<input type="submit" class="btn btn-outline-primary" value="Отправить заявку в команду" />
|
|
||||||
</form>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<h2 class="mt-4">Участники</h2>
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead class="table-primary">
|
|
||||||
<tr>
|
|
||||||
<th>Ник</th>
|
|
||||||
<th class="thin">Роль</th>
|
|
||||||
<th class="thin">Дата вступления</th>
|
|
||||||
{{ if $IsAdmin }}
|
|
||||||
<th class="thin">Действие</th>
|
|
||||||
{{ end }}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{ range .Team.Members }}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{{.User.Username}}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{if eq .Role 0 }}
|
|
||||||
Капитан
|
|
||||||
{{else if eq .Role 1 }}
|
|
||||||
Участник
|
|
||||||
{{end}}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ .CreatedAt.Format "15:04 02.01.2006" }}
|
|
||||||
</td>
|
|
||||||
{{ if and $IsAdmin (ne .Role 0) }}
|
|
||||||
<td>
|
|
||||||
<form method="post" action="/team/member/remove">
|
|
||||||
<input type="hidden" name="member_id" value="{{.User.ID}}">
|
|
||||||
<input type="submit" class="btn btn-outline-danger" value="Выгнать" />
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
{{ else if $IsAdmin }}
|
|
||||||
<td> </td>
|
|
||||||
{{ end }}
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Чтобы добавить участников в команду пришлите им ссылку на команду
|
|
||||||
(<a href="https://nquest.ru/team/{{.Team.ID}}">https://nquest.ru/team/{{.Team.ID}}</a>).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
По этой ссылке будущие участники могут подать заявку на вступление в команду.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{ if $IsAdmin }}
|
|
||||||
<h2>Заявки</h2>
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead class="table-primary">
|
|
||||||
<tr>
|
|
||||||
<th>Ник</th>
|
|
||||||
<th class="thin">Дата заявки</th>
|
|
||||||
<th class="thin">Действие</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{ range .Team.Requests }}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{{ .User.Username }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ .CreatedAt.Format "15:04 02.01.2006" }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<form method="post" action="/team/member/approve">
|
|
||||||
<input type="hidden" name="member_id" value="{{ .User.ID }}">
|
|
||||||
<div class="btn-group">
|
|
||||||
<input type="submit" name="approve" class="btn btn-outline-success" value="Принять" />
|
|
||||||
<input type="submit" name="decline" class="btn btn-outline-danger" value="Отказать" />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{else}}
|
|
||||||
<tr>
|
|
||||||
<td colspan="3"><span>Нет заявок</span></td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ if and .User.Team (eq .Team.ID .User.Team.TeamID) (eq .User.Team.Role 1) }}
|
|
||||||
<form method="post" action="/team/member/leave">
|
|
||||||
<input type="submit" class="btn btn-outline-danger" value="Покинуть команду" />
|
|
||||||
</form>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ template "footer" . }}
|
|
Loading…
Reference in a new issue