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:
|
||||
image: plugins/docker
|
||||
settings:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
10
api/types.go
10
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
|
||||
|
||||
|
|
|
@ -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(
|
||||
<Route
|
||||
|
@ -40,6 +43,19 @@ const router = createBrowserRouter(
|
|||
element={<Auth><Team /></Auth>}
|
||||
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>
|
||||
)
|
||||
|
@ -52,10 +68,10 @@ function App() {
|
|||
|
||||
function Auth({ children }) {
|
||||
const baseUser = useRouteLoaderData("root")
|
||||
const {user} = UserProvider.useContainer();
|
||||
const { user } = UserProvider.useContainer();
|
||||
const location = useLocation();
|
||||
if (!user && !baseUser) {
|
||||
return <Navigate to="/login" state={{from: location}} replace />;
|
||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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{
|
||||
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
|
||||
}
|
||||
|
|
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),
|
||||
Title: game.Title,
|
||||
Description: game.Description,
|
||||
StartAt: game.StartAt.Format("02.01.06"),
|
||||
StartAt: game.StartAt.Format("02.01.06 15:04"),
|
||||
Teams: teams,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue