Рефакторинг 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
|
||||
PG_NAME=nquest
|
||||
PG_USER=nquest
|
||||
PG_PASS=nquest
|
||||
PG_PORT=5432
|
||||
POSTGRES_HOSTNAME=localhost
|
||||
POSTGRES_DB=nquest
|
||||
POSTGRES_USER=nquest
|
||||
POSTGRES_PASSWORD=nquest
|
||||
POSTGRES_PORT=5432
|
||||
SECRET=s3cr3t
|
||||
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
|
||||
|
||||
import "gitrepo.ru/neonxp/nquest/pkg/models"
|
||||
import (
|
||||
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||
)
|
||||
|
||||
var MapTeamRole = map[models.Role]UserTeamRole{
|
||||
models.Captain: Captain,
|
||||
models.Member: Member,
|
||||
func MapGameType(typ GameType) models.GameType {
|
||||
switch typ {
|
||||
case City:
|
||||
return models.CityGame
|
||||
case Virtual:
|
||||
return models.VirtualGame
|
||||
}
|
||||
var MapTeamRoleReverse = map[UserTeamRole]models.Role{
|
||||
Captain: models.Captain,
|
||||
Member: models.Member,
|
||||
return 0
|
||||
}
|
||||
|
||||
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],
|
||||
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"
|
||||
400:
|
||||
$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:
|
||||
get:
|
||||
security: []
|
||||
responses:
|
||||
200:
|
||||
$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:
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -234,20 +98,6 @@ components:
|
|||
username:
|
||||
type: string
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -257,28 +107,122 @@ components:
|
|||
type: string
|
||||
description:
|
||||
type: string
|
||||
startAt:
|
||||
type: string
|
||||
teams:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/teamView"
|
||||
type:
|
||||
$ref: "#/components/schemas/gameType"
|
||||
required:
|
||||
- id
|
||||
- title
|
||||
- description
|
||||
- startAt
|
||||
- teams
|
||||
gameAdminListItem:
|
||||
- type
|
||||
taskView:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
title:
|
||||
type: string
|
||||
createdAt:
|
||||
text:
|
||||
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:
|
||||
login:
|
||||
required: true
|
||||
|
@ -308,6 +252,23 @@ components:
|
|||
password2:
|
||||
type: string
|
||||
required: [ username, email, password, password2 ]
|
||||
gameEditRequest:
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/gameEdit'
|
||||
enterCodeRequest:
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
required:
|
||||
- code
|
||||
responses:
|
||||
userResponse:
|
||||
description: ''
|
||||
|
@ -322,15 +283,21 @@ components:
|
|||
type: string
|
||||
email:
|
||||
type: string
|
||||
team:
|
||||
$ref: "#/components/schemas/userTeam"
|
||||
role:
|
||||
$ref: "#/components/schemas/userRole"
|
||||
experience:
|
||||
type: integer
|
||||
level:
|
||||
type: integer
|
||||
games:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/gameView"
|
||||
required:
|
||||
- id
|
||||
- username
|
||||
- email
|
||||
- role
|
||||
- experience
|
||||
- level
|
||||
- games
|
||||
errorResponse:
|
||||
description: ''
|
||||
content:
|
||||
|
@ -343,36 +310,6 @@ components:
|
|||
message:
|
||||
type: string
|
||||
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:
|
||||
description: ''
|
||||
content:
|
||||
|
@ -387,14 +324,12 @@ components:
|
|||
'application/json':
|
||||
schema:
|
||||
$ref: "#/components/schemas/gameView"
|
||||
gameAdminList:
|
||||
taskResponse:
|
||||
description: ''
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/gameAdminListItem"
|
||||
$ref: "#/components/schemas/taskView"
|
||||
securitySchemes:
|
||||
cookieAuth:
|
||||
type: apiKey
|
||||
|
|
246
api/server.go
246
api/server.go
|
@ -21,35 +21,17 @@ import (
|
|||
// ServerInterface represents all server handlers.
|
||||
type ServerInterface interface {
|
||||
|
||||
// (GET /admin/games)
|
||||
GetAdminGames(ctx echo.Context) error
|
||||
// (GET /engine/{uid})
|
||||
GameEngine(ctx echo.Context, uid int) error
|
||||
|
||||
// (POST /engine/{uid}/code)
|
||||
EnterCode(ctx echo.Context, uid int) error
|
||||
|
||||
// (GET /games)
|
||||
GetGames(ctx echo.Context) error
|
||||
|
||||
// (GET /teams)
|
||||
GetTeams(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
|
||||
// (POST /games)
|
||||
CreateGame(ctx echo.Context) error
|
||||
|
||||
// (GET /user)
|
||||
GetUser(ctx echo.Context) error
|
||||
|
@ -69,14 +51,39 @@ type ServerInterfaceWrapper struct {
|
|||
Handler ServerInterface
|
||||
}
|
||||
|
||||
// GetAdminGames converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetAdminGames(ctx echo.Context) error {
|
||||
// GameEngine converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GameEngine(ctx echo.Context) 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
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -89,141 +96,14 @@ func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// GetTeams converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetTeams(ctx echo.Context) error {
|
||||
// CreateGame converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) CreateGame(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
ctx.Set(CookieAuthScopes, []string{})
|
||||
ctx.Set(CookieAuthScopes, []string{"creator", "admin"})
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.GetTeams(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)
|
||||
err = w.Handler.CreateGame(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -293,16 +173,10 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
|||
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+"/teams", wrapper.GetTeams)
|
||||
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.POST(baseURL+"/games", wrapper.CreateGame)
|
||||
router.GET(baseURL+"/user", wrapper.GetUser)
|
||||
router.POST(baseURL+"/user/login", wrapper.PostUserLogin)
|
||||
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
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"H4sIAAAAAAAC/+RZwW7cNhD9lYLtUbA2iU+6uQgQGE2A1nVyMfZAS+M10xWpkpQDw9C/FzMitdKK0lLr",
|
||||
"jWM0J8vikHzzZuaRo31iuSorJUFaw7InpuHfGoz9XRUC6MVWbYTEh1xJC9LiI6+qrci5FUqmX42iYZPf",
|
||||
"Q8nxqdKqAm3dfCi52OKDfayAZcxYLeSGNQmruDHflC4Cg01CQISGgmU3bo3ejHXiZ6jbr5Bb1gynWF0D",
|
||||
"vdgIY0G/NPzd4NvgaG1AS17CYc87y2RMQn+XKELojamUNM43rZW+cm+ewVGuir4rQlrYgEZHSzCGbyL8",
|
||||
"pCV29mF3CjC5FhViYhnD9Te8hIuiFPKjMHaRB8JCSeB/03DHMvZruquDtDUz6WD5Swsl7umAca354xwu",
|
||||
"nHMUudHQvgj4FovIAi9PEWoN3EJxYYN5LYqpNChvQZto5xDsJ5ozdi9hE5WTeO1ats9VOynA4zBDBRac",
|
||||
"K0XvT2/LpEdNbPbi9ub7pglusSRNUG9OkCbTqjmVIlpt4ZAzBA7tHHcx9tdot0hyKdAB3SWEcaFtEscM",
|
||||
"kTFWkROVlRV2G+tQazufpa1uUbqMEA5cXIDRWK7thF+U/ydI5eOY6Hu0w+lRhdjpydLCCMZmN2arz3D8",
|
||||
"P2ZOy0ng3uBz9lDQ+yq40K8XxRhOzHmAea01SHvt1MKN3yq1BS4jj6zx4AIhcSIy710nbCidsi5xtlT2",
|
||||
"C2hxJ8DrkV9G4RNHQektNoyJ93dI1ZSv0wfq4rSdoWBCP3eA9xloQ4Be88ryGW/DiTHl7bEnQUD6E2Yg",
|
||||
"r7Wwj38jG/42rP4RcFHbe0KBJ0L7ylORMQPGtLLjhawSf4A7lIW8U4St1TQm/6LKTNgDaNOeMG/OVmcr",
|
||||
"9EVVIHklWMbenb05W1FLYO8JRkoZQvfE9hQCqhBkiM7wy4Jl7ANYOpk+kNVee/B2tZoKfmc3vCIPKGHZ",
|
||||
"zZCMm3H6NmuccRjk8/ANrlh7EFsA3Uk0BeCaDI4BML7kNQk7X707PHPYnlFuVMoE8P2pTA+g798fn3F/",
|
||||
"i6uPqbIYNZrRRA05Oj+Coy6a6RP+uXzftLeXLVgYM/ee3hN312RNFaR5CZbE/8YVMFbVrnytNx3210mP",
|
||||
"zn3ZadYjSs5H1yr2nNSYzdwXd+4HxjvtHd3Rcf/UdXT/O4YOqsYLMnAKbQp9TAj0RnO9vF/i1WtX2v+k",
|
||||
"ERXGq90HiZ+h1j0/6RPe05zaLyLqM837bnQlwZVqv+mPKCBeVVo9QKgf2qsTb/kK68R3nlPH3mfXXC6G",
|
||||
"OPgCdvR57CGm3c8n00mJUD+S2SjC4V17v9C49ZvTeLpa7mngMu39VrWNchztYm5Hps5zMOYXt/SpEfd/",
|
||||
"LJrHfOUtj4hXt8vrCdlsz7ZG3TGgH7wu1nrLMpZi19msm/8CAAD//7N+kRk/HAAA",
|
||||
"H4sIAAAAAAAC/8xYTY+kRgz9K5GTIxp6P07cktVqFGUPyWSTS6sPFfAwtQtVpFz0bmvEf49cfDQ0BU0T",
|
||||
"ZrWnGRXGfu/Zxq5+hljnhVaoLEH0DAb/LZHsLzqR6A5QWTTvdIIP9RM+i7WyqNy/oigyGQsrtQo/kVZ8",
|
||||
"RvET5oL/K4wu0NjGVawT5L/2VCBEQNZIlUJVBS6qNJhAtK+tDkFrpf/5hLGFamhmTYlVAKnI8X0i7Rps",
|
||||
"Pxl8hAh+DM8ChPVTClu/E2EznUr1P4TAXMjMo0QAhSD6ok1yXabaR++NhZIZTCVZNN8a/vnha+/TktAo",
|
||||
"kS8okM4yGIvQj7JIEHdChVbUcDNGm4fmZLtal8piioaJ5kgk0qWNcLb300mQYiMLxgQRQNMTHyTZVSSk",
|
||||
"xZyWdMffEr9wtAaSMEac5hCtQrMMhD+oFfR586DsdC4ol+YGxTPdYPi1QCNRxRPFxarQBokMQCb+CBke",
|
||||
"MfM/Wt7CkjvV08c9em2kltOy8q+CRtSuCd1nfOkounB4jUbfOJgaXPUDJ/M3gOGL302zUfz5MAEUWjZ7",
|
||||
"wTjb3AvLS42t64k6LjUrbeaXoT64XsQf2e5SltptcJEl57JF3zGcku1jAwBVmbPPozS2FFyVsbSn3mtn",
|
||||
"zF1T3az2VMu9kD6uCWdE8mlCOivZxl9O4rFZLDwc8Ku9Xsm1g8Z6Lr5f4I3i+wJ39evt4eV90H2SPH3Q",
|
||||
"klvubZAOX2f5WU+X1EQLOT9BQ7UPdEqq6c/dbVJNTSd3K8FkE1/rZZ/y+EKyt6TnE8CMMC6NtKc/GW8r",
|
||||
"vf4s8efSPjmWPC3rIwigntlASNRr/whEIX/DZquT6lE74DUDUH+4C1cARzRUT99Xd7u7HbPUBSpRSIjg",
|
||||
"zd2ru51byu2TgxGiSqXC8LmUScUHKTqluErcbvRrAhHc88Byhu5dI3K0aAiifQOd/Z2Bl+5LNlzsg95y",
|
||||
"dfk1qA4XS//r3W4q5Z1dOFgoKyfKgE3YDvNCk4fT+/Ye/YKU2qv7aZpN73Yfjq721Va6dGuoP71o753B",
|
||||
"mnCj602/4CHaH9za4svAO4PC4n29cd4s1uVPDdVa8FPAhz26h5jhap6GIsmlgkN1cNry2jwn7V/kRujt",
|
||||
"4AaXlyqAt7s3118a3peb9LOnsPuRxJ+N3zU5qB+c2YqE1P6rbZjubmd6UXV93rq0i4iz3Qj/29GqCFTG",
|
||||
"MRL90LjeGnH/J6F5zA+t5Yp8dVG+n5TNNuCBv6mE5th+pkuTQQQhT7bqUP0XAAD//zkXlqI5FQAA",
|
||||
}
|
||||
|
||||
// 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"
|
||||
)
|
||||
|
||||
// Defines values for UserRole.
|
||||
// Defines values for GameType.
|
||||
const (
|
||||
Admin UserRole = "admin"
|
||||
Creator UserRole = "creator"
|
||||
NotVerified UserRole = "notVerified"
|
||||
User UserRole = "user"
|
||||
City GameType = "city"
|
||||
Virtual GameType = "virtual"
|
||||
)
|
||||
|
||||
// Defines values for UserTeamRole.
|
||||
const (
|
||||
Captain UserTeamRole = "captain"
|
||||
Member UserTeamRole = "member"
|
||||
)
|
||||
|
||||
// GameAdminListItem defines model for gameAdminListItem.
|
||||
type GameAdminListItem struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Id int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
// CodeEdit defines model for codeEdit.
|
||||
type CodeEdit struct {
|
||||
Code string `json:"code"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
type GameView struct {
|
||||
Description string `json:"description"`
|
||||
Id int `json:"id"`
|
||||
StartAt string `json:"startAt"`
|
||||
Teams []TeamView `json:"teams"`
|
||||
Title string `json:"title"`
|
||||
Type GameType `json:"type"`
|
||||
}
|
||||
|
||||
// SolutionEdit defines model for solutionEdit.
|
||||
type SolutionEdit struct {
|
||||
After int `json:"after"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// SolutionView defines model for solutionView.
|
||||
type SolutionView struct {
|
||||
After int `json:"after"`
|
||||
Text *string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
// TaskEdit defines model for taskEdit.
|
||||
type TaskEdit struct {
|
||||
Codes []CodeEdit `json:"codes"`
|
||||
Solutions []SolutionEdit `json:"solutions"`
|
||||
Text string `json:"text"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// TeamMember defines model for teamMember.
|
||||
type TeamMember struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Role UserTeamRole `json:"role"`
|
||||
User UserView `json:"user"`
|
||||
}
|
||||
|
||||
// TeamRequest defines model for teamRequest.
|
||||
type TeamRequest struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
User UserView `json:"user"`
|
||||
}
|
||||
|
||||
// TeamView defines model for teamView.
|
||||
type TeamView struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
CurrentTeam *bool `json:"currentTeam,omitempty"`
|
||||
Id int `json:"id"`
|
||||
Members *int `json:"members,omitempty"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// UserRole defines model for userRole.
|
||||
type UserRole string
|
||||
|
||||
// UserTeam defines model for userTeam.
|
||||
type UserTeam struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
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"`
|
||||
// TaskView defines model for taskView.
|
||||
type TaskView struct {
|
||||
Codes []CodeView `json:"codes"`
|
||||
Entered []CodeView `json:"entered"`
|
||||
Solutions []SolutionView `json:"solutions"`
|
||||
Text string `json:"text"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// ErrorResponse defines model for errorResponse.
|
||||
|
@ -84,33 +80,33 @@ type ErrorResponse struct {
|
|||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// GameAdminList defines model for gameAdminList.
|
||||
type GameAdminList = []GameAdminListItem
|
||||
|
||||
// GameListResponse defines model for gameListResponse.
|
||||
type GameListResponse = []GameView
|
||||
|
||||
// TeamResponse defines model for teamResponse.
|
||||
type TeamResponse struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Id int `json:"id"`
|
||||
Members []TeamMember `json:"members"`
|
||||
Name string `json:"name"`
|
||||
Requests []TeamRequest `json:"requests"`
|
||||
}
|
||||
// GameResponse defines model for gameResponse.
|
||||
type GameResponse = GameView
|
||||
|
||||
// TeamsListResponse defines model for teamsListResponse.
|
||||
type TeamsListResponse = []TeamView
|
||||
// TaskResponse defines model for taskResponse.
|
||||
type TaskResponse = TaskView
|
||||
|
||||
// UserResponse defines model for userResponse.
|
||||
type UserResponse struct {
|
||||
Email string `json:"email"`
|
||||
Experience int `json:"experience"`
|
||||
Games []GameView `json:"games"`
|
||||
Id int `json:"id"`
|
||||
Role UserRole `json:"role"`
|
||||
Team *UserTeam `json:"team,omitempty"`
|
||||
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.
|
||||
type Login struct {
|
||||
Email string `json:"email"`
|
||||
|
@ -125,19 +121,9 @@ type Register struct {
|
|||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// PostTeamsJSONBody defines parameters for PostTeams.
|
||||
type PostTeamsJSONBody struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// PostTeamsTeamIDMembersJSONBody defines parameters for PostTeamsTeamIDMembers.
|
||||
type PostTeamsTeamIDMembersJSONBody struct {
|
||||
Members []int `json:"members"`
|
||||
}
|
||||
|
||||
// PostTeamsTeamIDRequestsUserIDJSONBody defines parameters for PostTeamsTeamIDRequestsUserID.
|
||||
type PostTeamsTeamIDRequestsUserIDJSONBody struct {
|
||||
Approve bool `json:"approve"`
|
||||
// EnterCodeJSONBody defines parameters for EnterCode.
|
||||
type EnterCodeJSONBody struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// PostUserLoginJSONBody defines parameters for PostUserLogin.
|
||||
|
@ -154,14 +140,11 @@ type PostUserRegisterJSONBody struct {
|
|||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// PostTeamsJSONRequestBody defines body for PostTeams for application/json ContentType.
|
||||
type PostTeamsJSONRequestBody PostTeamsJSONBody
|
||||
// EnterCodeJSONRequestBody defines body for EnterCode for application/json ContentType.
|
||||
type EnterCodeJSONRequestBody EnterCodeJSONBody
|
||||
|
||||
// PostTeamsTeamIDMembersJSONRequestBody defines body for PostTeamsTeamIDMembers for application/json ContentType.
|
||||
type PostTeamsTeamIDMembersJSONRequestBody PostTeamsTeamIDMembersJSONBody
|
||||
|
||||
// PostTeamsTeamIDRequestsUserIDJSONRequestBody defines body for PostTeamsTeamIDRequestsUserID for application/json ContentType.
|
||||
type PostTeamsTeamIDRequestsUserIDJSONRequestBody PostTeamsTeamIDRequestsUserIDJSONBody
|
||||
// CreateGameJSONRequestBody defines body for CreateGame for application/json ContentType.
|
||||
type CreateGameJSONRequestBody = GameEdit
|
||||
|
||||
// PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType.
|
||||
type PostUserLoginJSONRequestBody PostUserLoginJSONBody
|
||||
|
|
10
config.go
10
config.go
|
@ -7,11 +7,11 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
PgHost string `envconfig:"PG_HOST"`
|
||||
PgName string `envconfig:"PG_NAME"`
|
||||
PgUser string `envconfig:"PG_USER"`
|
||||
PgPass string `envconfig:"PG_PASS"`
|
||||
PgPort int `envconfig:"PG_PORT"`
|
||||
PgHost string `envconfig:"POSTGRES_HOSTNAME"`
|
||||
PgName string `envconfig:"POSTGRES_DB"`
|
||||
PgUser string `envconfig:"POSTGRES_USER"`
|
||||
PgPass string `envconfig:"POSTGRES_PASSWORD"`
|
||||
PgPort int `envconfig:"POSTGRES_PORT"`
|
||||
Listen string `envconfig:"LISTEN"`
|
||||
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
|
32
main.go
32
main.go
|
@ -3,7 +3,6 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
|
@ -50,13 +49,8 @@ func main() {
|
|||
|
||||
if err := db.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.Team{},
|
||||
&models.TeamMember{},
|
||||
&models.TeamRequest{},
|
||||
&models.Game{},
|
||||
&models.GamePassing{},
|
||||
&models.Team{},
|
||||
&models.TeamAtGame{},
|
||||
&models.GameCursor{},
|
||||
&models.Task{},
|
||||
&models.Solution{},
|
||||
&models.Code{},
|
||||
|
@ -67,8 +61,8 @@ func main() {
|
|||
|
||||
// --[ Services ]--
|
||||
userService := service.NewUser(db)
|
||||
teamService := service.NewTeam(db, userService)
|
||||
gameService := service.NewGame(db)
|
||||
engineService := service.NewEngine(db)
|
||||
|
||||
// --[ HTTP server ]--
|
||||
|
||||
|
@ -108,14 +102,14 @@ func main() {
|
|||
session.Middleware(store),
|
||||
middleware.Logger(),
|
||||
middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)),
|
||||
middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
TokenLookup: "cookie:_csrf",
|
||||
CookiePath: "/",
|
||||
// CookieDomain: "nquest.ru",
|
||||
// CookieSecure: true,
|
||||
CookieHTTPOnly: true,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
}),
|
||||
// middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
// TokenLookup: "cookie:_csrf",
|
||||
// CookiePath: "/",
|
||||
// // CookieDomain: "nquest.ru",
|
||||
// // CookieSecure: true,
|
||||
// CookieHTTPOnly: true,
|
||||
// CookieSameSite: http.SameSiteStrictMode,
|
||||
// }),
|
||||
middleware.Gzip(),
|
||||
echoprometheus.NewMiddleware("nquest"),
|
||||
appmiddleware.User(userService),
|
||||
|
@ -129,12 +123,9 @@ func main() {
|
|||
User: &controller.User{
|
||||
UserService: userService,
|
||||
},
|
||||
Team: &controller.Team{
|
||||
UserService: userService,
|
||||
TeamService: teamService,
|
||||
},
|
||||
Engine: &controller.Engine{
|
||||
GameService: gameService,
|
||||
EngineService: engineService,
|
||||
},
|
||||
Admin: &controller.Admin{
|
||||
GameService: gameService,
|
||||
|
@ -168,7 +159,6 @@ func main() {
|
|||
type serverRouter struct {
|
||||
*controller.Game
|
||||
*controller.User
|
||||
*controller.Team
|
||||
*controller.Engine
|
||||
*controller.Admin
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
|
@ -13,20 +14,63 @@ type Admin struct {
|
|||
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)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
result := make(api.GameAdminList, 0, len(games))
|
||||
for _, g := range games {
|
||||
result = append(result, api.GameAdminListItem{
|
||||
Id: int(g.ID),
|
||||
Title: g.Title,
|
||||
CreatedAt: g.CreatedAt.Format("02.01.06 15:04"),
|
||||
|
||||
return ctx.JSON(http.StatusCreated, api.GameResponse{
|
||||
Id: int(game.ID),
|
||||
Title: game.Title,
|
||||
Description: game.Description,
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, result)
|
||||
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 game
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ package controller
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
|
@ -12,44 +12,68 @@ import (
|
|||
|
||||
type Engine struct {
|
||||
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)
|
||||
team := user.Team.Team
|
||||
gameID, err := strconv.Atoi(c.Param("ID"))
|
||||
|
||||
game, err := ec.GameService.GetByID(c.Request().Context(), uint(uid))
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
state, err := ec.GameService.GetState(c.Request().Context(), game, team)
|
||||
return c.JSON(http.StatusOK, mapCursorToTask(cursor))
|
||||
}
|
||||
|
||||
// (POST /engine/{uid}/code)
|
||||
func (ec *Engine) EnterCode(c echo.Context, uid int) error {
|
||||
user := contextlib.GetUser(c)
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
game, err := ec.GameService.GetByID(ctx, uint(uid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
history, err := ec.GameService.GetHistory(c.Request().Context(), game, team)
|
||||
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.Render(
|
||||
http.StatusOK,
|
||||
"engine_view",
|
||||
&GetReponse{
|
||||
Game: game,
|
||||
State: state,
|
||||
History: history,
|
||||
},
|
||||
)
|
||||
return c.JSON(http.StatusOK, mapCursorToTask(cursor))
|
||||
}
|
||||
|
||||
type GetReponse struct {
|
||||
Game *models.Game
|
||||
State *models.GamePassing
|
||||
History []*models.GamePassing
|
||||
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"
|
||||
"gitrepo.ru/neonxp/nquest/api"
|
||||
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
|
||||
"gitrepo.ru/neonxp/nquest/pkg/service"
|
||||
)
|
||||
|
||||
|
@ -20,31 +19,12 @@ func (g *Game) GetGames(ctx echo.Context) error {
|
|||
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))
|
||||
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{
|
||||
Id: int(game.ID),
|
||||
Title: game.Title,
|
||||
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,13 +34,11 @@ func (u *User) PostUserLogin(c echo.Context) error {
|
|||
}
|
||||
|
||||
if err := setUser(c, user); err != nil {
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &api.ErrorResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return mapUser(c, user)
|
||||
}
|
||||
|
@ -95,16 +93,6 @@ func (u *User) GetUser(c echo.Context) error {
|
|||
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 {
|
||||
sess, err := session.Get("session", c)
|
||||
if err != nil {
|
||||
|
@ -136,3 +124,26 @@ func setUser(c echo.Context, user *models.User) error {
|
|||
|
||||
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
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
Model
|
||||
|
||||
Visible bool `gorm:"index"`
|
||||
Title string
|
||||
Description string
|
||||
StartAt time.Time
|
||||
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"`
|
||||
Type GameType
|
||||
Points int
|
||||
}
|
||||
|
||||
type TeamAtGame 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"`
|
||||
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
|
||||
type GameType int
|
||||
|
||||
const (
|
||||
PassStarted Passing = iota
|
||||
PassFinished
|
||||
PassCanceled
|
||||
PassFailed
|
||||
VirtualGame GameType = iota
|
||||
CityGame
|
||||
)
|
||||
|
|
|
@ -6,7 +6,6 @@ type Task struct {
|
|||
Title string
|
||||
Text string
|
||||
MaxTime int
|
||||
Game *Game
|
||||
GameID uint
|
||||
Solutions []*Solution `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"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyPassword = errors.New("empty password")
|
||||
)
|
||||
var ErrEmptyPassword = errors.New("empty password")
|
||||
|
||||
type User struct {
|
||||
Model
|
||||
Username string `gorm:"unique" json:"username"`
|
||||
Email string `gorm:"unique" json:"email"`
|
||||
Password string `json:"-"`
|
||||
Team *TeamMember `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"team"`
|
||||
Role UserRole `json:"role"`
|
||||
Experience int
|
||||
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 (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrGameNotStarted = errors.New("game not started")
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
@ -30,7 +23,7 @@ func (gs *Game) GetByID(ctx context.Context, id uint) (*models.Game, error) {
|
|||
|
||||
return g, gs.DB.
|
||||
WithContext(ctx).
|
||||
Preload("FirstTask").
|
||||
Preload("Tasks").
|
||||
First(g, id).
|
||||
Error
|
||||
}
|
||||
|
@ -40,9 +33,7 @@ func (gs *Game) List(ctx context.Context) ([]*models.Game, error) {
|
|||
|
||||
return games, gs.DB.
|
||||
WithContext(ctx).
|
||||
Preload("Teams").
|
||||
Preload("Teams.Team").
|
||||
Order("start_at DESC").
|
||||
Order("created_at DESC").
|
||||
Find(&games, "visible = true").
|
||||
Limit(20).
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
games := make([]*models.Game, 0)
|
||||
|
||||
return games, gs.DB.
|
||||
WithContext(ctx).
|
||||
Model(&models.Game{}).
|
||||
Preload("Teams").
|
||||
Preload("Teams.Team").
|
||||
Preload("Authors", gs.DB.Where("id = ?", author.ID)).
|
||||
Order("created_at DESC").
|
||||
Find(&games).
|
||||
Limit(20).
|
||||
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,
|
||||
Email: normalizer.NewNormalizer().Normalize(email),
|
||||
Password: hex.EncodeToString(hashed),
|
||||
Role: models.RoleUser,
|
||||
}
|
||||
|
||||
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.
|
||||
WithContext(ctx).
|
||||
Where("email = ?", nemail).
|
||||
Preload("Team").Preload("Team.Team").
|
||||
First(u).
|
||||
Error
|
||||
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) {
|
||||
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 {
|
||||
|
|
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