From 439ba778122a952c46681846cbc68f7c6046a704 Mon Sep 17 00:00:00 2001 From: NeonXP Date: Sun, 19 Nov 2023 22:54:54 +0300 Subject: [PATCH] new version --- .devcontainer/Dockerfile | 100 ++++++++++++++++++++++++++++++ .devcontainer/devcontainer.json | 25 ++++++++ .forgejo/workflows/build.yaml | 6 -- .woodpecker.yaml | 2 +- api/openapi.yaml | 26 +++++++- api/server.go | 51 ++++++++++------ api/types.go | 10 +++ frontend/src/App.jsx | 26 ++++++-- frontend/src/pages/Admin.jsx | 31 ++++++++++ frontend/src/pages/AdminGame.jsx | 102 +++++++++++++++++++++++++++++++ frontend/src/utils/roles.js | 2 +- main.go | 4 ++ pkg/controller/admin.go | 32 ++++++++++ pkg/controller/game.go | 2 +- pkg/controller/user.go | 18 ++---- pkg/models/game.go | 1 + pkg/service/game.go | 17 +++++- 17 files changed, 409 insertions(+), 46 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json delete mode 100644 .forgejo/workflows/build.yaml create mode 100644 frontend/src/pages/Admin.jsx create mode 100644 frontend/src/pages/AdminGame.jsx create mode 100644 pkg/controller/admin.go diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..f6b74a3 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,100 @@ +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 new file mode 100644 index 0000000..b49eeb7 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +{ + "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/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml deleted file mode 100644 index d470cda..0000000 --- a/.forgejo/workflows/build.yaml +++ /dev/null @@ -1,6 +0,0 @@ -on: [push] -jobs: - test: - runs-on: docker - steps: - - run: echo All Good diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 1393438..b813be7 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -1,4 +1,4 @@ -pipeline: +steps: build: image: plugins/docker settings: diff --git a/api/openapi.yaml b/api/openapi.yaml index 4a7dad0..2428d32 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -168,7 +168,13 @@ paths: $ref: '#/components/responses/teamResponse' 404: $ref: '#/components/responses/errorResponse' - + /admin/games: + get: + security: + - cookieAuth: [creator, admin] + responses: + 200: + $ref: "#/components/responses/gameAdminList" # Game routes /games: @@ -263,6 +269,16 @@ components: - description - startAt - teams + gameAdminListItem: + type: object + properties: + id: + type: integer + title: + type: string + createdAt: + type: string + required: [ id, title, createdAt ] requestBodies: login: required: true @@ -371,6 +387,14 @@ components: 'application/json': schema: $ref: "#/components/schemas/gameView" + gameAdminList: + description: '' + content: + 'application/json': + schema: + type: array + items: + $ref: "#/components/schemas/gameAdminListItem" securitySchemes: cookieAuth: type: apiKey diff --git a/api/server.go b/api/server.go index 1f84b91..51c1d14 100644 --- a/api/server.go +++ b/api/server.go @@ -21,6 +21,9 @@ import ( // ServerInterface represents all server handlers. type ServerInterface interface { + // (GET /admin/games) + GetAdminGames(ctx echo.Context) error + // (GET /games) GetGames(ctx echo.Context) error @@ -66,6 +69,17 @@ type ServerInterfaceWrapper struct { Handler ServerInterface } +// GetAdminGames converts echo context to params. +func (w *ServerInterfaceWrapper) GetAdminGames(ctx echo.Context) error { + var err error + + ctx.Set(CookieAuthScopes, []string{"creator", "admin"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetAdminGames(ctx) + return err +} + // GetGames converts echo context to params. func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error { var err error @@ -279,6 +293,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL Handler: si, } + router.GET(baseURL+"/admin/games", wrapper.GetAdminGames) router.GET(baseURL+"/games", wrapper.GetGames) router.GET(baseURL+"/teams", wrapper.GetTeams) router.POST(baseURL+"/teams", wrapper.PostTeams) @@ -298,24 +313,24 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+RZwW7cNhD9lYLtUbA2iU+6pQgQBE2A1nVyMfZAS+M10xXJkiMHhqF/L4YitZJFran1", - "ZmM0p8jkkHzz+OaR3DywUtVaSZBoWfHADPzbgMXfVSXANWzVRkj6KJVEkEifXOutKDkKJfOvVrluW95C", - "zelLG6XBoB8PNRdb+sB7DaxgFo2QG9ZmTHNrvylTRTrbzAERBipWXPk5BiPWWRihrr9CiawdD0HTgGvY", - "CItgTg1/1/k62ttYMJLX8HTmfWQ2JWG4ShIhrsVqJa3PzRhlLnzLMzgqVTVMRUiEDRhKtAZr+SYhTzfF", - "Lj6eTgW2NEITJlYwmn/Da/goLB6UhECoHf7fDNywgv2a70oh78JsTit8EfCNVvOQuDH8fg4RAq+PQakB", - "jlC9xah+RDVHd30NxiYnR2A/uTHT9DI2o9AseMSydS66QREex0oQJGwv+ZDPYMlsQE2qSmh5+31lQkss", - "kQnV9RFkMu9OcxIxagtPJePAUZznLiX+kuIWWZvb6Ii/OYRpW9tmnhlHRl+qE5pG4xaQZZEbnKlBJ6oj", - "6CNjKHCbylcXO2ZihzOgmrLX9fhaX+g2qZIhCQTZ0N8pYzpOIodeEML+eg9+21nLwrxOijEuzP0Ay8YY", - "kHjpS9D3Xyu1BS4Tz4Fp54Lq9JW5P7veLciPZFPTaKnwCxhxIyAUeZhG0RevaiEHk433JOQ7pmou1/lT", - "arFs91AwY0o7wI8Z6LaAsuYa+Z5s48KYy/ZQe434acYslI0ReP83sRGucuofAW8bvHUoyGa7pkBFwSxY", - "29lOMDIt/gB/0gl5oxy2ztOY/MtVZsbuwNjOtl+drc5WlIvSILkWrGBvzl6drdx9Fm8dDHft6kwdXG0Q", - "N+5I/FCxgr0HfO8CHl1oX69Wczvex+WTO+OQClZcrenvvLf3OQCXLuAQANPrSJux89Wbp0eOL+yOcK1s", - "BN+fyg4Ahhfd/TNuGmmim9Pa5OmRTNSYo/MDOOp3M3+gfz68a7srwRYQpsy9c+2Ou0sX7WRpeA3oHPXK", - "VwVJdVcTGELHL65sQOfjWm7XE0rOJ3cV9hxp7FXuyZP7gfudD87D5H3/1L89/ncMPekaJ2TgGN4Ue/ZO", - "j869r84wxYv3rnz4+E7axovd0/lnqPXAT/5Alx/v9ouI+uzGfTe6suhMTVj0RxQQ19qoO4g9Mh7VSYh8", - "gXUSnnNzx95n/2JbDHH0W83B53GAmPc/qM+LkqB+dGGTHY6vOvjN3s/fHifT1fJMI5fpkLdqMClxiku5", - "HdmmLMHaX/zUx0Y8/O+D/ZgvQuQB+9Wv8nK2bNwwfhVercl3LJi74IuN2bKC5fSUa9ftfwEAAP//8qqL", - "tlEaAAA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/types.go b/api/types.go index 30ba941..9efd156 100644 --- a/api/types.go +++ b/api/types.go @@ -21,6 +21,13 @@ const ( Member UserTeamRole = "member" ) +// GameAdminListItem defines model for gameAdminListItem. +type GameAdminListItem struct { + CreatedAt string `json:"createdAt"` + Id int `json:"id"` + Title string `json:"title"` +} + // GameView defines model for gameView. type GameView struct { Description string `json:"description"` @@ -77,6 +84,9 @@ type ErrorResponse struct { Message string `json:"message"` } +// GameAdminList defines model for gameAdminList. +type GameAdminList = []GameAdminListItem + // GameListResponse defines model for gameListResponse. type GameListResponse = []GameView diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6174829..00dedcf 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,6 +11,9 @@ import Teams from './pages/Teams' import { UserProvider } from './store/user' import { ajax } from './utils/fetch' import TeamNew from './pages/TeamNew' +import Admin from './pages/Admin' +import AdminGame from './pages/AdminGame' + const router = createBrowserRouter( createRoutesFromElements( } loader={async () => ajax("/api/user")} > - } loader={() => ajax("/api/games")} /> @@ -40,6 +43,19 @@ const router = createBrowserRouter( element={} loader={({ params }) => ajax(`/api/teams/${params.teamId}`)} /> + } + loader={() => ajax(`/api/admin/games`)} + /> + } + loader={() => ({ + title: "Новая игра", + tasks: [] + })} + /> } /> ) @@ -52,12 +68,12 @@ function App() { function Auth({ children }) { const baseUser = useRouteLoaderData("root") - const {user} = UserProvider.useContainer(); + const { user } = UserProvider.useContainer(); const location = useLocation(); if (!user && !baseUser) { - return ; + return ; } - + return children; } diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx new file mode 100644 index 0000000..1f92ca8 --- /dev/null +++ b/frontend/src/pages/Admin.jsx @@ -0,0 +1,31 @@ +import { Link, useLoaderData } from "react-router-dom"; +import { Button, Table } from 'react-bootstrap'; + +export default () => { + const games = useLoaderData(); + if (!games) { + return null + } + + return (<> +

Управление играми

+ + + + + + + + + + {games.map(game => ( + + + + + + ))} + +
IDНазваниеСоздана
{game.id}{game.title}{team.createdAt}
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/AdminGame.jsx b/frontend/src/pages/AdminGame.jsx new file mode 100644 index 0000000..26d9b0b --- /dev/null +++ b/frontend/src/pages/AdminGame.jsx @@ -0,0 +1,102 @@ +import { useLoaderData } from "react-router-dom"; +import { Col, Row, Form, Button, Card } from 'react-bootstrap'; +import { useEffect, useState } from "react"; + +export default () => { + const loadedGame = useLoaderData(); + const [game, setGame] = useState(loadedGame); + const [error, setError] = useState(null); + + if (!game) { + return null + } + const submit = (e) => { + e.preventDefault(); + console.log(game) + } + return (<> +

Игра "{game.title}"

+
+
+ {error ? (
{error}
) : null} + + + Название игры + + setGame({ ...game, title: e.target.value })} + /> + + + + Начало в + + + + + + Описание + + setGame({ ...game, description: e.target.value })} + /> + + +
+

+ Задания + +

+ {game.tasks.map((task, idx) => + { + const newTasks = game.tasks; + newTasks[idx] = task; + setGame({...game, tasks: newTasks}); + }} + /> + )} +
+ +
+
+ ); +} + +const Task = ({id, task, setTask}) => ( + + + + Задание #{id + 1} + + + Задание + + setTask({ ...task, text: e.target.value })} + /> + + + + +) \ No newline at end of file diff --git a/frontend/src/utils/roles.js b/frontend/src/utils/roles.js index 41ee59e..c352fc2 100644 --- a/frontend/src/utils/roles.js +++ b/frontend/src/utils/roles.js @@ -19,6 +19,6 @@ export const useRole = () => { const { user } = UserProvider.useContainer(); return { - hasRole: (role) => user && !!roleHierarchy[user.role][role] + hasRole: (role) => user && user.role && !!roleHierarchy[user.role][role] } } \ No newline at end of file diff --git a/main.go b/main.go index c52f668..29f8675 100644 --- a/main.go +++ b/main.go @@ -136,6 +136,9 @@ func main() { Engine: &controller.Engine{ GameService: gameService, }, + Admin: &controller.Admin{ + GameService: gameService, + }, } codegen := e.Group("") @@ -167,4 +170,5 @@ type serverRouter struct { *controller.User *controller.Team *controller.Engine + *controller.Admin } diff --git a/pkg/controller/admin.go b/pkg/controller/admin.go new file mode 100644 index 0000000..6927ac5 --- /dev/null +++ b/pkg/controller/admin.go @@ -0,0 +1,32 @@ +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/service" +) + +type Admin struct { + GameService *service.Game +} + +func (a *Admin) GetAdminGames(ctx echo.Context) error { + user := contextlib.GetUser(ctx) + games, err := a.GameService.ListByAuthor(ctx.Request().Context(), user) + 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.StatusOK, result) +} diff --git a/pkg/controller/game.go b/pkg/controller/game.go index a1f3d3e..29cbce1 100644 --- a/pkg/controller/game.go +++ b/pkg/controller/game.go @@ -43,7 +43,7 @@ func (g *Game) GetGames(ctx echo.Context) error { Id: int(game.ID), Title: game.Title, Description: game.Description, - StartAt: game.StartAt.Format("02.01.06"), + StartAt: game.StartAt.Format("02.01.06 15:04"), Teams: teams, }) } diff --git a/pkg/controller/user.go b/pkg/controller/user.go index 38dee48..d917880 100644 --- a/pkg/controller/user.go +++ b/pkg/controller/user.go @@ -42,12 +42,7 @@ func (u *User) PostUserLogin(c echo.Context) error { } } - return c.JSON(http.StatusOK, &api.UserResponse{ - Id: int(user.ID), - Username: user.Username, - Email: user.Email, - Team: api.MapUserTeam(user.Team), - }) + return mapUser(c, user) } func (u *User) PostUserRegister(c echo.Context) error { @@ -77,12 +72,7 @@ func (u *User) PostUserRegister(c echo.Context) error { }) } - return c.JSON(http.StatusOK, &api.UserResponse{ - Id: int(user.ID), - Username: user.Username, - Email: user.Email, - Team: api.MapUserTeam(user.Team), - }) + return mapUser(c, user) } func (u *User) PostUserLogout(c echo.Context) error { @@ -102,6 +92,10 @@ 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, diff --git a/pkg/models/game.go b/pkg/models/game.go index 90d16f5..84dc899 100644 --- a/pkg/models/game.go +++ b/pkg/models/game.go @@ -15,6 +15,7 @@ type Game struct { Tasks []*Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` FirstTask *Task `gorm:"foreignKey:ID"` FirstTaskID uint + Authors []*User `gorm:"many2many:game_authors"` } type TeamAtGame struct { diff --git a/pkg/service/game.go b/pkg/service/game.go index bc8bc0f..8b977cb 100644 --- a/pkg/service/game.go +++ b/pkg/service/game.go @@ -42,7 +42,7 @@ func (gs *Game) List(ctx context.Context) ([]*models.Game, error) { WithContext(ctx). Preload("Teams"). Preload("Teams.Team"). - Order("start_at ASC"). + Order("start_at DESC"). Find(&games, "visible = true"). Limit(20). Error @@ -129,3 +129,18 @@ func (gs *Game) GetState(ctx context.Context, game *models.Game, team *models.Te 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 +}