From 2e1db4ac60ca7139ba382cdabb09dc7647049797 Mon Sep 17 00:00:00 2001
From: NeonXP
Date: Fri, 5 Jan 2024 03:50:33 +0300
Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?=
=?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20api?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.devcontainer/Dockerfile | 100 --------
.devcontainer/devcontainer.json | 25 --
.env | 10 +-
.vscode/launch.json | 16 ++
.vscode/settings.json | 14 ++
api/mapper.go | 53 ++--
api/openapi.yaml | 411 ++++++++++++++------------------
api/server.go | 246 +++++--------------
api/types.go | 167 ++++++-------
config.go | 10 +-
docker-compose.yml | 35 +++
main.go | 34 +--
pkg/controller/admin.go | 64 ++++-
pkg/controller/engine.go | 82 ++++---
pkg/controller/game.go | 20 --
pkg/controller/team.go | 211 ----------------
pkg/controller/user.go | 43 ++--
pkg/models/cursor.go | 24 ++
pkg/models/game.go | 47 +---
pkg/models/task.go | 1 -
pkg/models/team.go | 37 ---
pkg/models/user.go | 23 +-
pkg/service/engine.go | 123 ++++++++++
pkg/service/game.go | 109 ++-------
pkg/service/team.go | 209 ----------------
pkg/service/user.go | 9 +-
requests.http | 132 ++++++++++
views/team/list.gotmpl | 35 ---
views/team/view.gotmpl | 130 ----------
29 files changed, 865 insertions(+), 1555 deletions(-)
delete mode 100644 .devcontainer/Dockerfile
delete mode 100644 .devcontainer/devcontainer.json
create mode 100644 .vscode/launch.json
create mode 100644 .vscode/settings.json
create mode 100644 docker-compose.yml
delete mode 100644 pkg/controller/team.go
create mode 100644 pkg/models/cursor.go
delete mode 100644 pkg/models/team.go
create mode 100644 pkg/service/engine.go
delete mode 100644 pkg/service/team.go
create mode 100644 requests.http
delete mode 100644 views/team/list.gotmpl
delete mode 100644 views/team/view.gotmpl
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
deleted file mode 100644
index f6b74a3..0000000
--- a/.devcontainer/Dockerfile
+++ /dev/null
@@ -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"
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
deleted file mode 100644
index b49eeb7..0000000
--- a/.devcontainer/devcontainer.json
+++ /dev/null
@@ -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"
- ]
-}
\ No newline at end of file
diff --git a/.env b/.env
index d4aa3b2..7544bf3 100644
--- a/.env
+++ b/.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
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..eac9d30
--- /dev/null
+++ b/.vscode/launch.json
@@ -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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..9c9032f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,14 @@
+{
+ "sqltools.connections": [
+ {
+ "previewLimit": 50,
+ "server": "localhost",
+ "port": 5432,
+ "driver": "PostgreSQL",
+ "name": "localhost",
+ "database": "nquest",
+ "username": "nquest",
+ "password": "nquest"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/api/mapper.go b/api/mapper.go
index b53ecb6..d0f7064 100644
--- a/api/mapper.go
+++ b/api/mapper.go
@@ -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 ""
}
diff --git a/api/openapi.yaml b/api/openapi.yaml
index 2428d32..aa7e655 100644
--- a/api/openapi.yaml
+++ b/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
diff --git a/api/server.go b/api/server.go
index 51c1d14..b911cd5 100644
--- a/api/server.go
+++ b/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
diff --git a/api/types.go b/api/types.go
index 9efd156..6c2a65c 100644
--- a/api/types.go
+++ b/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"`
+ 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
diff --git a/config.go b/config.go
index 89b858b..96215ab 100644
--- a/config.go
+++ b/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"`
}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..8b1d7ed
--- /dev/null
+++ b/docker-compose.yml
@@ -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
diff --git a/main.go b/main.go
index 29f8675..f71aa0a 100644
--- a/main.go
+++ b/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,
+ 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
}
diff --git a/pkg/controller/admin.go b/pkg/controller/admin.go
index 6927ac5..fe13302 100644
--- a/pkg/controller/admin.go
+++ b/pkg/controller/admin.go
@@ -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
}
diff --git a/pkg/controller/engine.go b/pkg/controller/engine.go
index 98117ff..7ffbf68 100644
--- a/pkg/controller/engine.go
+++ b/pkg/controller/engine.go
@@ -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
}
diff --git a/pkg/controller/game.go b/pkg/controller/game.go
index 29cbce1..3e48425 100644
--- a/pkg/controller/game.go
+++ b/pkg/controller/game.go
@@ -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,
})
}
diff --git a/pkg/controller/team.go b/pkg/controller/team.go
deleted file mode 100644
index b187163..0000000
--- a/pkg/controller/team.go
+++ /dev/null
@@ -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)
-}
diff --git a/pkg/controller/user.go b/pkg/controller/user.go
index d917880..e204fa2 100644
--- a/pkg/controller/user.go
+++ b/pkg/controller/user.go
@@ -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,
+ })
+}
diff --git a/pkg/models/cursor.go b/pkg/models/cursor.go
new file mode 100644
index 0000000..63d7548
--- /dev/null
+++ b/pkg/models/cursor.go
@@ -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
+)
diff --git a/pkg/models/game.go b/pkg/models/game.go
index 84dc899..d375efd 100644
--- a/pkg/models/game.go
+++ b/pkg/models/game.go
@@ -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
)
diff --git a/pkg/models/task.go b/pkg/models/task.go
index 9b86a68..2ce5de8 100644
--- a/pkg/models/task.go
+++ b/pkg/models/task.go
@@ -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;"`
diff --git a/pkg/models/team.go b/pkg/models/team.go
deleted file mode 100644
index e880f1c..0000000
--- a/pkg/models/team.go
+++ /dev/null
@@ -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
-)
diff --git a/pkg/models/user.go b/pkg/models/user.go
index e415a5c..5d480d4 100644
--- a/pkg/models/user.go
+++ b/pkg/models/user.go
@@ -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
-)
diff --git a/pkg/service/engine.go b/pkg/service/engine.go
new file mode 100644
index 0000000..ab9e4bd
--- /dev/null
+++ b/pkg/service/engine.go
@@ -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
+}
diff --git a/pkg/service/game.go b/pkg/service/game.go
index 8b977cb..5bc8192 100644
--- a/pkg/service/game.go
+++ b/pkg/service/game.go
@@ -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
+ })
+}
diff --git a/pkg/service/team.go b/pkg/service/team.go
deleted file mode 100644
index d1747cc..0000000
--- a/pkg/service/team.go
+++ /dev/null
@@ -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
-}
diff --git a/pkg/service/user.go b/pkg/service/user.go
index cb11981..55c658d 100644
--- a/pkg/service/user.go
+++ b/pkg/service/user.go
@@ -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 {
diff --git a/requests.http b/requests.http
new file mode 100644
index 0000000..680d575
--- /dev/null
+++ b/requests.http
@@ -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"
+}
\ No newline at end of file
diff --git a/views/team/list.gotmpl b/views/team/list.gotmpl
deleted file mode 100644
index 739c2f8..0000000
--- a/views/team/list.gotmpl
+++ /dev/null
@@ -1,35 +0,0 @@
-{{ template "header" . }}
-Команды
-{{$MyTeam := 0}}
-{{if and .User .User.Team }}
-{{$MyTeam = .User.Team.Team.ID}}
-{{end}}
-
-{{if and .User (not .User.Team) }}
-
-
-
-{{ template "footer" . }}
\ No newline at end of file
diff --git a/views/team/view.gotmpl b/views/team/view.gotmpl
deleted file mode 100644
index 3bc5257..0000000
--- a/views/team/view.gotmpl
+++ /dev/null
@@ -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 }}
-
-
-
-
-
- {{range .Teams}}
- Название
- Количество участников
-
-
- {{end}}
-
-
-
- {{.Name}}
-
-
-
- {{len .Members}}
-
- {{ .Team.Name }}
-
Создана {{ .Team.CreatedAt.Format "02.01.2006" }}
- -{{ if $IsAdmin }} -Вы капитан команды
-{{ 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 }} -Ник | -Роль | -Дата вступления | - {{ if $IsAdmin }} -Действие | - {{ end }} -|
---|---|---|---|---|
- {{.User.Username}} - | -- {{if eq .Role 0 }} - Капитан - {{else if eq .Role 1 }} - Участник - {{end}} - | -- {{ .CreatedAt.Format "15:04 02.01.2006" }} - | - {{ if and $IsAdmin (ne .Role 0) }} -- - | - {{ else if $IsAdmin }} -- {{ end }} - |
- Чтобы добавить участников в команду пришлите им ссылку на команду - (https://nquest.ru/team/{{.Team.ID}}). -
- -- По этой ссылке будущие участники могут подать заявку на вступление в команду. -
- -{{ if $IsAdmin }} -Ник | -Дата заявки | -Действие | -
---|---|---|
- {{ .User.Username }} - | -- {{ .CreatedAt.Format "15:04 02.01.2006" }} - | -- - | -
Нет заявок | -