Рефакторинг api

This commit is contained in:
Александр Кирюхин 2024-01-05 03:50:33 +03:00
parent 439ba77812
commit 2e1db4ac60
29 changed files with 865 additions and 1555 deletions

View file

@ -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"

View file

@ -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
View file

@ -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
View 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
View file

@ -0,0 +1,14 @@
{
"sqltools.connections": [
{
"previewLimit": 50,
"server": "localhost",
"port": 5432,
"driver": "PostgreSQL",
"name": "localhost",
"database": "nquest",
"username": "nquest",
"password": "nquest"
}
]
}

View file

@ -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,
}
var MapTeamRoleReverse = map[UserTeamRole]models.Role{
Captain: models.Captain,
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],
func MapGameType(typ GameType) models.GameType {
switch typ {
case City:
return models.CityGame
case Virtual:
return models.VirtualGame
}
return 0
}
func MapGameTypeReverse(typ models.GameType) GameType {
switch typ {
case models.CityGame:
return City
case models.VirtualGame:
return Virtual
}
return ""
}

View file

@ -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

View file

@ -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{
"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",
}
// GetSwagger returns the content of the embedded swagger specification file

View file

@ -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"`
Description string `json:"description"`
Id int `json:"id"`
Title string `json:"title"`
Type GameType `json:"type"`
}
// TeamMember defines model for teamMember.
type TeamMember struct {
CreatedAt string `json:"createdAt"`
Role UserTeamRole `json:"role"`
User UserView `json:"user"`
// SolutionEdit defines model for solutionEdit.
type SolutionEdit struct {
After int `json:"after"`
Text string `json:"text"`
}
// TeamRequest defines model for teamRequest.
type TeamRequest struct {
CreatedAt string `json:"createdAt"`
User UserView `json:"user"`
// SolutionView defines model for solutionView.
type SolutionView struct {
After int `json:"after"`
Text *string `json:"text,omitempty"`
}
// 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"`
// TaskEdit defines model for taskEdit.
type TaskEdit struct {
Codes []CodeEdit `json:"codes"`
Solutions []SolutionEdit `json:"solutions"`
Text string `json:"text"`
Title string `json:"title"`
}
// 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"`
Id int `json:"id"`
Role UserRole `json:"role"`
Team *UserTeam `json:"team,omitempty"`
Username string `json:"username"`
Email string `json:"email"`
Experience int `json:"experience"`
Games []GameView `json:"games"`
Id int `json:"id"`
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

View file

@ -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
View 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
View file

@ -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,
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
}

View file

@ -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,
})
}
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
}

View file

@ -2,54 +2,78 @@ 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"
)
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)
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)
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,
},
)
return c.JSON(http.StatusOK, mapCursorToTask(cursor))
}
type GetReponse struct {
Game *models.Game
State *models.GamePassing
History []*models.GamePassing
// (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
}
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
}

View file

@ -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,
})
}

View file

@ -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)
}

View file

@ -34,12 +34,10 @@ 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 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
View 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
)

View file

@ -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
Tasks []*Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
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
)

View file

@ -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;"`

View file

@ -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
)

View file

@ -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"`
Username string `gorm:"unique" json:"username"`
Email string `gorm:"unique" json:"email"`
Password string `json:"-"`
Experience int
Games []*GameCursor
}
type UserRole int
const (
RoleNotVerified UserRole = iota
RoleUser
RoleCreator
RoleAdmin
)

123
pkg/service/engine.go Normal file
View 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
}

View file

@ -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
})
}

View file

@ -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
}

View file

@ -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
View 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"
}

View file

@ -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" . }}

View file

@ -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>&nbsp;</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" . }}