new version
This commit is contained in:
parent
3833ada27c
commit
439ba77812
17 changed files with 409 additions and 46 deletions
100
.devcontainer/Dockerfile
Normal file
100
.devcontainer/Dockerfile
Normal file
|
@ -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"
|
25
.devcontainer/devcontainer.json
Normal file
25
.devcontainer/devcontainer.json
Normal file
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
on: [push]
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: docker
|
|
||||||
steps:
|
|
||||||
- run: echo All Good
|
|
|
@ -1,4 +1,4 @@
|
||||||
pipeline:
|
steps:
|
||||||
build:
|
build:
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
|
|
|
@ -168,7 +168,13 @@ paths:
|
||||||
$ref: '#/components/responses/teamResponse'
|
$ref: '#/components/responses/teamResponse'
|
||||||
404:
|
404:
|
||||||
$ref: '#/components/responses/errorResponse'
|
$ref: '#/components/responses/errorResponse'
|
||||||
|
/admin/games:
|
||||||
|
get:
|
||||||
|
security:
|
||||||
|
- cookieAuth: [creator, admin]
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: "#/components/responses/gameAdminList"
|
||||||
# Game routes
|
# Game routes
|
||||||
|
|
||||||
/games:
|
/games:
|
||||||
|
@ -263,6 +269,16 @@ components:
|
||||||
- description
|
- description
|
||||||
- startAt
|
- startAt
|
||||||
- teams
|
- teams
|
||||||
|
gameAdminListItem:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
required: [ id, title, createdAt ]
|
||||||
requestBodies:
|
requestBodies:
|
||||||
login:
|
login:
|
||||||
required: true
|
required: true
|
||||||
|
@ -371,6 +387,14 @@ components:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/gameView"
|
$ref: "#/components/schemas/gameView"
|
||||||
|
gameAdminList:
|
||||||
|
description: ''
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/gameAdminListItem"
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
cookieAuth:
|
cookieAuth:
|
||||||
type: apiKey
|
type: apiKey
|
||||||
|
|
|
@ -21,6 +21,9 @@ import (
|
||||||
// ServerInterface represents all server handlers.
|
// ServerInterface represents all server handlers.
|
||||||
type ServerInterface interface {
|
type ServerInterface interface {
|
||||||
|
|
||||||
|
// (GET /admin/games)
|
||||||
|
GetAdminGames(ctx echo.Context) error
|
||||||
|
|
||||||
// (GET /games)
|
// (GET /games)
|
||||||
GetGames(ctx echo.Context) error
|
GetGames(ctx echo.Context) error
|
||||||
|
|
||||||
|
@ -66,6 +69,17 @@ type ServerInterfaceWrapper struct {
|
||||||
Handler ServerInterface
|
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.
|
// GetGames converts echo context to params.
|
||||||
func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
|
func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
|
@ -279,6 +293,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||||
Handler: si,
|
Handler: si,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.GET(baseURL+"/admin/games", wrapper.GetAdminGames)
|
||||||
router.GET(baseURL+"/games", wrapper.GetGames)
|
router.GET(baseURL+"/games", wrapper.GetGames)
|
||||||
router.GET(baseURL+"/teams", wrapper.GetTeams)
|
router.GET(baseURL+"/teams", wrapper.GetTeams)
|
||||||
router.POST(baseURL+"/teams", wrapper.PostTeams)
|
router.POST(baseURL+"/teams", wrapper.PostTeams)
|
||||||
|
@ -298,24 +313,24 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"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",
|
||||||
"tlEaAAA=",
|
"LJrHfOUtj4hXt8vrCdlsz7ZG3TGgH7wu1nrLMpZi19msm/8CAAD//7N+kRk/HAAA",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|
10
api/types.go
10
api/types.go
|
@ -21,6 +21,13 @@ const (
|
||||||
Member UserTeamRole = "member"
|
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.
|
// GameView defines model for gameView.
|
||||||
type GameView struct {
|
type GameView struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
@ -77,6 +84,9 @@ type ErrorResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GameAdminList defines model for gameAdminList.
|
||||||
|
type GameAdminList = []GameAdminListItem
|
||||||
|
|
||||||
// GameListResponse defines model for gameListResponse.
|
// GameListResponse defines model for gameListResponse.
|
||||||
type GameListResponse = []GameView
|
type GameListResponse = []GameView
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,9 @@ import Teams from './pages/Teams'
|
||||||
import { UserProvider } from './store/user'
|
import { UserProvider } from './store/user'
|
||||||
import { ajax } from './utils/fetch'
|
import { ajax } from './utils/fetch'
|
||||||
import TeamNew from './pages/TeamNew'
|
import TeamNew from './pages/TeamNew'
|
||||||
|
import Admin from './pages/Admin'
|
||||||
|
import AdminGame from './pages/AdminGame'
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
<Route
|
<Route
|
||||||
|
@ -19,8 +22,8 @@ const router = createBrowserRouter(
|
||||||
element={<Layout />}
|
element={<Layout />}
|
||||||
loader={async () => ajax("/api/user")}
|
loader={async () => ajax("/api/user")}
|
||||||
>
|
>
|
||||||
<Route
|
<Route
|
||||||
index
|
index
|
||||||
element={<Index />}
|
element={<Index />}
|
||||||
loader={() => ajax("/api/games")}
|
loader={() => ajax("/api/games")}
|
||||||
/>
|
/>
|
||||||
|
@ -40,6 +43,19 @@ const router = createBrowserRouter(
|
||||||
element={<Auth><Team /></Auth>}
|
element={<Auth><Team /></Auth>}
|
||||||
loader={({ params }) => ajax(`/api/teams/${params.teamId}`)}
|
loader={({ params }) => ajax(`/api/teams/${params.teamId}`)}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="admin"
|
||||||
|
element={<Auth role="creator"><Admin /></Auth>}
|
||||||
|
loader={() => ajax(`/api/admin/games`)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="admin/games/new"
|
||||||
|
element={<Auth role="creator"><AdminGame /></Auth>}
|
||||||
|
loader={() => ({
|
||||||
|
title: "Новая игра",
|
||||||
|
tasks: []
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<Route path="*" element={<NoMatch />} />
|
<Route path="*" element={<NoMatch />} />
|
||||||
</Route>
|
</Route>
|
||||||
)
|
)
|
||||||
|
@ -52,12 +68,12 @@ function App() {
|
||||||
|
|
||||||
function Auth({ children }) {
|
function Auth({ children }) {
|
||||||
const baseUser = useRouteLoaderData("root")
|
const baseUser = useRouteLoaderData("root")
|
||||||
const {user} = UserProvider.useContainer();
|
const { user } = UserProvider.useContainer();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
if (!user && !baseUser) {
|
if (!user && !baseUser) {
|
||||||
return <Navigate to="/login" state={{from: location}} replace />;
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
31
frontend/src/pages/Admin.jsx
Normal file
31
frontend/src/pages/Admin.jsx
Normal file
|
@ -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 (<>
|
||||||
|
<h1>Управление играми <Button to="/admin/games/new" as={Link}>Создать игру</Button> </h1>
|
||||||
|
<Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Название</th>
|
||||||
|
<th>Создана</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{games.map(game => (
|
||||||
|
<tr key={game.id}>
|
||||||
|
<td>{game.id}</td>
|
||||||
|
<td>{game.title}</td>
|
||||||
|
<td>{team.createdAt}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</>);
|
||||||
|
}
|
102
frontend/src/pages/AdminGame.jsx
Normal file
102
frontend/src/pages/AdminGame.jsx
Normal file
|
@ -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 (<>
|
||||||
|
<h1>Игра "{game.title}"</h1>
|
||||||
|
<Form onSubmit={submit}>
|
||||||
|
<div className="col-lg-10 px-0">
|
||||||
|
{error ? (<div className="alert alert-danger" role="alert">{error}</div>) : null}
|
||||||
|
|
||||||
|
<Form.Group as={Row} className="mb-3" controlId="title">
|
||||||
|
<Form.Label column sm="4">Название игры</Form.Label>
|
||||||
|
<Col sm="8">
|
||||||
|
<Form.Control
|
||||||
|
name="title"
|
||||||
|
type="text"
|
||||||
|
value={game.title}
|
||||||
|
onChange={e => setGame({ ...game, title: e.target.value })}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group as={Row} className="mb-3" controlId="startAt">
|
||||||
|
<Form.Label column sm="4">Начало в</Form.Label>
|
||||||
|
<Col sm="8">
|
||||||
|
<Form.Control
|
||||||
|
name="startAt"
|
||||||
|
type="datetime-local"
|
||||||
|
value={game.startAt}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group as={Row} className="mb-3" controlId="description">
|
||||||
|
<Form.Label column sm="4">Описание</Form.Label>
|
||||||
|
<Col sm="8">
|
||||||
|
<Form.Control
|
||||||
|
name="description"
|
||||||
|
as="textarea"
|
||||||
|
rows={5}
|
||||||
|
value={game.description}
|
||||||
|
onChange={e => setGame({ ...game, description: e.target.value })}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
Задания
|
||||||
|
<Button onClick={() => setGame({ ...game, tasks: [...game.tasks, { title: "Задание" }] })}>
|
||||||
|
Добавить задание
|
||||||
|
</Button>
|
||||||
|
</h2>
|
||||||
|
{game.tasks.map((task, idx) =>
|
||||||
|
<Task
|
||||||
|
key={idx}
|
||||||
|
id={idx}
|
||||||
|
task={task}
|
||||||
|
setTask={task => {
|
||||||
|
const newTasks = game.tasks;
|
||||||
|
newTasks[idx] = task;
|
||||||
|
setGame({...game, tasks: newTasks});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button type="submit" size="lg">Сохранить</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Task = ({id, task, setTask}) => (
|
||||||
|
<Card>
|
||||||
|
<Card.Body>
|
||||||
|
<Card.Title>
|
||||||
|
Задание #{id + 1}
|
||||||
|
</Card.Title>
|
||||||
|
<Form.Group as={Row} className="mb-3" controlId={`task[${id}].text`}>
|
||||||
|
<Form.Label column sm="4">Задание</Form.Label>
|
||||||
|
<Col sm="8">
|
||||||
|
<Form.Control
|
||||||
|
name={`task[${id}].text`}
|
||||||
|
as="textarea"
|
||||||
|
rows={5}
|
||||||
|
value={task.text}
|
||||||
|
onChange={e => setTask({ ...task, text: e.target.value })}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
)
|
|
@ -19,6 +19,6 @@ export const useRole = () => {
|
||||||
const { user } = UserProvider.useContainer();
|
const { user } = UserProvider.useContainer();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasRole: (role) => user && !!roleHierarchy[user.role][role]
|
hasRole: (role) => user && user.role && !!roleHierarchy[user.role][role]
|
||||||
}
|
}
|
||||||
}
|
}
|
4
main.go
4
main.go
|
@ -136,6 +136,9 @@ func main() {
|
||||||
Engine: &controller.Engine{
|
Engine: &controller.Engine{
|
||||||
GameService: gameService,
|
GameService: gameService,
|
||||||
},
|
},
|
||||||
|
Admin: &controller.Admin{
|
||||||
|
GameService: gameService,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
codegen := e.Group("")
|
codegen := e.Group("")
|
||||||
|
|
||||||
|
@ -167,4 +170,5 @@ type serverRouter struct {
|
||||||
*controller.User
|
*controller.User
|
||||||
*controller.Team
|
*controller.Team
|
||||||
*controller.Engine
|
*controller.Engine
|
||||||
|
*controller.Admin
|
||||||
}
|
}
|
||||||
|
|
32
pkg/controller/admin.go
Normal file
32
pkg/controller/admin.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -43,7 +43,7 @@ func (g *Game) GetGames(ctx echo.Context) error {
|
||||||
Id: int(game.ID),
|
Id: int(game.ID),
|
||||||
Title: game.Title,
|
Title: game.Title,
|
||||||
Description: game.Description,
|
Description: game.Description,
|
||||||
StartAt: game.StartAt.Format("02.01.06"),
|
StartAt: game.StartAt.Format("02.01.06 15:04"),
|
||||||
Teams: teams,
|
Teams: teams,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,12 +42,7 @@ func (u *User) PostUserLogin(c echo.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, &api.UserResponse{
|
return mapUser(c, user)
|
||||||
Id: int(user.ID),
|
|
||||||
Username: user.Username,
|
|
||||||
Email: user.Email,
|
|
||||||
Team: api.MapUserTeam(user.Team),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) PostUserRegister(c echo.Context) error {
|
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{
|
return mapUser(c, user)
|
||||||
Id: int(user.ID),
|
|
||||||
Username: user.Username,
|
|
||||||
Email: user.Email,
|
|
||||||
Team: api.MapUserTeam(user.Team),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) PostUserLogout(c echo.Context) error {
|
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{
|
return c.JSON(http.StatusOK, &api.UserResponse{
|
||||||
Id: int(user.ID),
|
Id: int(user.ID),
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
|
|
|
@ -15,6 +15,7 @@ type Game struct {
|
||||||
Tasks []*Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
Tasks []*Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
FirstTask *Task `gorm:"foreignKey:ID"`
|
FirstTask *Task `gorm:"foreignKey:ID"`
|
||||||
FirstTaskID uint
|
FirstTaskID uint
|
||||||
|
Authors []*User `gorm:"many2many:game_authors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TeamAtGame struct {
|
type TeamAtGame struct {
|
||||||
|
|
|
@ -42,7 +42,7 @@ func (gs *Game) List(ctx context.Context) ([]*models.Game, error) {
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Preload("Teams").
|
Preload("Teams").
|
||||||
Preload("Teams.Team").
|
Preload("Teams.Team").
|
||||||
Order("start_at ASC").
|
Order("start_at DESC").
|
||||||
Find(&games, "visible = true").
|
Find(&games, "visible = true").
|
||||||
Limit(20).
|
Limit(20).
|
||||||
Error
|
Error
|
||||||
|
@ -129,3 +129,18 @@ func (gs *Game) GetState(ctx context.Context, game *models.Game, team *models.Te
|
||||||
|
|
||||||
return gamepass, nil
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue