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) }} -
- Создать команду -
-{{end}} - - - - - - - - - {{range .Teams}} - - - - - {{end}} - -
НазваниеКоличество участников
- - {{.Name}} - - - {{len .Members}} -
-{{ 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 }} - -

{{ .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 }} -
Заявка в команду рассматривается.
- {{ else }} -
- -
- {{ end }} -{{ end }} - -

Участники

- - - - - - - {{ if $IsAdmin }} - - {{ end }} - - - - {{ range .Team.Members }} - - - - - {{ if and $IsAdmin (ne .Role 0) }} - - {{ else if $IsAdmin }} - - {{ end }} - - {{ end }} - -
НикРольДата вступленияДействие
- {{.User.Username}} - - {{if eq .Role 0 }} - Капитан - {{else if eq .Role 1 }} - Участник - {{end}} - - {{ .CreatedAt.Format "15:04 02.01.2006" }} - -
- - -
-
 
- - -

- Чтобы добавить участников в команду пришлите им ссылку на команду - (https://nquest.ru/team/{{.Team.ID}}). -

- -

- По этой ссылке будущие участники могут подать заявку на вступление в команду. -

- -{{ if $IsAdmin }} -

Заявки

- - - - - - - - - - {{ range .Team.Requests }} - - - - - - {{else}} - - - - {{ end }} - -
НикДата заявкиДействие
- {{ .User.Username }} - - {{ .CreatedAt.Format "15:04 02.01.2006" }} - -
- -
- - -
-
-
Нет заявок
-{{ end }} - -{{ if and .User.Team (eq .Team.ID .User.Team.TeamID) (eq .User.Team.Role 1) }} -
- -
-{{ end }} - -{{ template "footer" . }} \ No newline at end of file