Поддержка ролей пользователя
This commit is contained in:
parent
8f6748d2ca
commit
d0fce2b2fe
18 changed files with 570 additions and 148 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.sql
|
9
Makefile
9
Makefile
|
@ -2,10 +2,9 @@
|
|||
db:
|
||||
docker run --name nquest-db -e POSTGRES_DB=nquest -e POSTGRES_USER=nquest -e POSTGRES_PASSWORD=nquest -p 5432:5432 -d postgres
|
||||
|
||||
.PHONY: gen-api
|
||||
gen-api:
|
||||
oapi-codegen -generate server,spec -package api -o api/server.go api/openapi.yaml
|
||||
oapi-codegen -generate types -package api -o api/types.go api/openapi.yaml
|
||||
.PHONY: generate
|
||||
generate:
|
||||
go generate ./...
|
||||
|
||||
.PHONY: build-front
|
||||
build-front:
|
||||
|
@ -13,4 +12,4 @@ build-front:
|
|||
|
||||
.PHONY: dev-front
|
||||
dev-front:
|
||||
cd frontend & npm run dev
|
||||
cd frontend; npm run dev
|
4
api/doc.go
Normal file
4
api/doc.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package api
|
||||
|
||||
//go:generate go run github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen -generate server,spec -package api -o ./server.go ./openapi.yaml
|
||||
//go:generate go run github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen -generate types -package api -o ./types.go ./openapi.yaml
|
|
@ -2,10 +2,28 @@ package api
|
|||
|
||||
import "gitrepo.ru/neonxp/nquest/pkg/models"
|
||||
|
||||
var MapRole = map[models.Role]UserTeamRole{
|
||||
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 {
|
||||
|
@ -15,6 +33,6 @@ func MapUserTeam(team *models.TeamMember) *UserTeam {
|
|||
return &UserTeam{
|
||||
Id: int(team.Team.ID),
|
||||
Name: team.Team.Name,
|
||||
Role: MapRole[team.Role],
|
||||
Role: MapTeamRole[team.Role],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
openapi: "3.1.0"
|
||||
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: nQuest
|
||||
|
||||
servers:
|
||||
- url: /api
|
||||
|
||||
paths:
|
||||
# User routes
|
||||
/user:
|
||||
|
@ -97,6 +100,17 @@ paths:
|
|||
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'
|
||||
|
@ -109,12 +123,46 @@ paths:
|
|||
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:
|
||||
userID:
|
||||
type: integer
|
||||
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'
|
||||
|
@ -147,6 +195,13 @@ components:
|
|||
enum:
|
||||
- member
|
||||
- captain
|
||||
userRole:
|
||||
type: string
|
||||
enum:
|
||||
- notVerified
|
||||
- user
|
||||
- creator
|
||||
- admin
|
||||
teamMember:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -253,10 +308,13 @@ components:
|
|||
type: string
|
||||
team:
|
||||
$ref: "#/components/schemas/userTeam"
|
||||
role:
|
||||
$ref: "#/components/schemas/userRole"
|
||||
required:
|
||||
- id
|
||||
- username
|
||||
- email
|
||||
- role
|
||||
errorResponse:
|
||||
description: ''
|
||||
content:
|
||||
|
|
|
@ -42,6 +42,12 @@ type ServerInterface interface {
|
|||
// (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
|
||||
|
||||
// (GET /user)
|
||||
GetUser(ctx echo.Context) error
|
||||
|
||||
|
@ -163,6 +169,50 @@ func (w *ServerInterfaceWrapper) PostTeamsTeamIDMembers(ctx echo.Context) error
|
|||
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)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUser converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetUser(ctx echo.Context) error {
|
||||
var err error
|
||||
|
@ -236,6 +286,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
|||
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.GET(baseURL+"/user", wrapper.GetUser)
|
||||
router.POST(baseURL+"/user/login", wrapper.PostUserLogin)
|
||||
router.POST(baseURL+"/user/logout", wrapper.PostUserLogout)
|
||||
|
@ -246,22 +298,24 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
|||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"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",
|
||||
"tlEaAAA=",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
|
30
api/types.go
30
api/types.go
|
@ -7,6 +7,14 @@ const (
|
|||
CookieAuthScopes = "cookieAuth.Scopes"
|
||||
)
|
||||
|
||||
// Defines values for UserRole.
|
||||
const (
|
||||
Admin UserRole = "admin"
|
||||
Creator UserRole = "creator"
|
||||
NotVerified UserRole = "notVerified"
|
||||
User UserRole = "user"
|
||||
)
|
||||
|
||||
// Defines values for UserTeamRole.
|
||||
const (
|
||||
Captain UserTeamRole = "captain"
|
||||
|
@ -44,6 +52,9 @@ type TeamView struct {
|
|||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// UserRole defines model for userRole.
|
||||
type UserRole string
|
||||
|
||||
// UserTeam defines model for userTeam.
|
||||
type UserTeam struct {
|
||||
Id int `json:"id"`
|
||||
|
@ -85,6 +96,7 @@ type TeamsListResponse = []TeamView
|
|||
type UserResponse struct {
|
||||
Email string `json:"email"`
|
||||
Id int `json:"id"`
|
||||
Role UserRole `json:"role"`
|
||||
Team *UserTeam `json:"team,omitempty"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
@ -108,8 +120,15 @@ type PostTeamsJSONBody struct {
|
|||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// DeleteTeamsTeamIDMembersJSONBody defines parameters for DeleteTeamsTeamIDMembers.
|
||||
type DeleteTeamsTeamIDMembersJSONBody = interface{}
|
||||
// PostTeamsTeamIDMembersJSONBody defines parameters for PostTeamsTeamIDMembers.
|
||||
type PostTeamsTeamIDMembersJSONBody struct {
|
||||
Members []int `json:"members"`
|
||||
}
|
||||
|
||||
// PostTeamsTeamIDRequestsUserIDJSONBody defines parameters for PostTeamsTeamIDRequestsUserID.
|
||||
type PostTeamsTeamIDRequestsUserIDJSONBody struct {
|
||||
Approve bool `json:"approve"`
|
||||
}
|
||||
|
||||
// PostUserLoginJSONBody defines parameters for PostUserLogin.
|
||||
type PostUserLoginJSONBody struct {
|
||||
|
@ -128,8 +147,11 @@ type PostUserRegisterJSONBody struct {
|
|||
// PostTeamsJSONRequestBody defines body for PostTeams for application/json ContentType.
|
||||
type PostTeamsJSONRequestBody PostTeamsJSONBody
|
||||
|
||||
// DeleteTeamsTeamIDMembersJSONRequestBody defines body for DeleteTeamsTeamIDMembers for application/json ContentType.
|
||||
type DeleteTeamsTeamIDMembersJSONRequestBody = DeleteTeamsTeamIDMembersJSONBody
|
||||
// PostTeamsTeamIDMembersJSONRequestBody defines body for PostTeamsTeamIDMembers for application/json ContentType.
|
||||
type PostTeamsTeamIDMembersJSONRequestBody PostTeamsTeamIDMembersJSONBody
|
||||
|
||||
// PostTeamsTeamIDRequestsUserIDJSONRequestBody defines body for PostTeamsTeamIDRequestsUserID for application/json ContentType.
|
||||
type PostTeamsTeamIDRequestsUserIDJSONRequestBody PostTeamsTeamIDRequestsUserIDJSONBody
|
||||
|
||||
// PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType.
|
||||
type PostUserLoginJSONRequestBody PostUserLoginJSONBody
|
||||
|
|
|
@ -2,9 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
|
@ -32,10 +30,5 @@ func (c *Config) DSN() string {
|
|||
func GetConfig() (*Config, error) {
|
||||
c := new(Config)
|
||||
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Println("not .env")
|
||||
}
|
||||
|
||||
return c, envconfig.Process("", c)
|
||||
}
|
||||
|
|
|
@ -3,9 +3,12 @@ import { Button, Nav, Container, NavbarBrand, NavbarToggle, Navbar, NavbarCollap
|
|||
import { UserProvider } from "../store/user";
|
||||
import { useEffect } from "react";
|
||||
import { ajax } from "../utils/fetch";
|
||||
import { useRole } from "../utils/roles";
|
||||
|
||||
export default () => {
|
||||
const params = useLoaderData();
|
||||
const { hasRole } = useRole();
|
||||
|
||||
useEffect(() => {
|
||||
setUser(params)
|
||||
}, [params])
|
||||
|
@ -28,28 +31,35 @@ export default () => {
|
|||
<Nav.Item>
|
||||
<Nav.Link as={Link} className="nav-link" to="/">Игры</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link as={Link} className="nav-link" to="/teams">Команды</Nav.Link>
|
||||
</Nav.Item>
|
||||
{hasRole("user") ? (<>
|
||||
<Nav.Item>
|
||||
<Nav.Link as={Link} className="nav-link" to="/teams">Команды</Nav.Link>
|
||||
</Nav.Item>
|
||||
{hasRole("creator") ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link as={Link} className="nav-link" to="/admin">Админка</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : null}
|
||||
</>) : null}
|
||||
</Nav>
|
||||
<Navbar.Text>
|
||||
{user ? (
|
||||
<>
|
||||
{user.username}
|
||||
{user.team ? (
|
||||
<>(<Link to={`teams/${user.team.id}`}>{user.team.name}</Link>)</>
|
||||
) : (
|
||||
<>(без команды)</>
|
||||
)}
|
||||
<Button type="button" variant="outline-success" onClick={logout}>Выход</Button>
|
||||
</>
|
||||
) : (
|
||||
<ButtonGroup>
|
||||
<Link className="btn btn-success" to="login">Вход</Link>
|
||||
<Link className="btn btn-outline-success" to="register">Регистрация</Link>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</Navbar.Text>
|
||||
{user ? (
|
||||
<>
|
||||
{user.username}
|
||||
{user.team ? (
|
||||
<>(<Link to={`teams/${user.team.id}`}>{user.team.name}</Link>)</>
|
||||
) : (
|
||||
<>(без команды)</>
|
||||
)}
|
||||
<Button type="button" variant="outline-success" onClick={logout}>Выход</Button>
|
||||
</>
|
||||
) : (
|
||||
<ButtonGroup>
|
||||
<Link className="btn btn-success" to="login">Вход</Link>
|
||||
<Link className="btn btn-outline-success" to="register">Регистрация</Link>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</Navbar.Text>
|
||||
</NavbarCollapse>
|
||||
</Container>
|
||||
</Navbar>
|
||||
|
|
|
@ -1,22 +1,32 @@
|
|||
import { useLoaderData, useNavigate, useRouteLoaderData } from "react-router-dom";
|
||||
import { Button, Table } from "react-bootstrap";
|
||||
import { Alert, Button, ButtonGroup, Table } from "react-bootstrap";
|
||||
import { UserProvider } from "../store/user";
|
||||
import { ajax } from "../utils/fetch";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const userRoles = { captain: "Капитан", member: "Участник" };
|
||||
|
||||
export default () => {
|
||||
const team = useLoaderData();
|
||||
const teamFromRouter = useLoaderData();
|
||||
const [team, setTeam] = useState(teamFromRouter);
|
||||
const [error, setError] = useState(null);
|
||||
useEffect(() => {
|
||||
setTeam(teamFromRouter);
|
||||
}, [teamFromRouter]);
|
||||
const navigate = useNavigate();
|
||||
const {request, setRequest} = useState(false);
|
||||
|
||||
const { user, setUser } = UserProvider.useContainer();
|
||||
|
||||
const [request, setRequest] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setRequest(!!team.requests.find(x => user && x.user.id == user.id));
|
||||
}, [user, team.requests])
|
||||
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { user } = UserProvider.useContainer();
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
@ -26,14 +36,38 @@ export default () => {
|
|||
const isCaptain = member && user.team.role == "captain";
|
||||
|
||||
const sendRequest = () => {
|
||||
ajax(`/api/teams/${team.id}/requests`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(() => setRequest(true)).catch(setError);
|
||||
};
|
||||
const updateMemebers = (members) => {
|
||||
ajax(`/api/teams/${team.id}/members`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then(() => setRequest(true));
|
||||
};
|
||||
body: JSON.stringify({ members }),
|
||||
}).then(team => setTeam(team)).catch(setError);
|
||||
};
|
||||
const approveRequest = (userID, approve) => {
|
||||
ajax(`/api/teams/${team.id}/requests/${userID}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ approve }),
|
||||
}).then(team => setTeam(team)).catch(setError);
|
||||
}
|
||||
const removeMember = (userID) => {
|
||||
const members = team.members.map(x => x.user.id).filter(uid => uid != userID);
|
||||
return updateMemebers(members)
|
||||
}
|
||||
const leaveTeam = () => {
|
||||
ajax(`/api/teams/${team.id}/members`, {
|
||||
method: "DELETE",
|
||||
|
@ -41,8 +75,13 @@ export default () => {
|
|||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then(() => navigate("/teams"));
|
||||
};
|
||||
}).then(team => setTeam(team)).then(() => {
|
||||
setUser({
|
||||
...user,
|
||||
team: null,
|
||||
});
|
||||
}).catch(setError);
|
||||
};
|
||||
const deleteTeam = () => {
|
||||
ajax(`/api/teams/${team.id}`, {
|
||||
method: "DELETE",
|
||||
|
@ -50,7 +89,13 @@ export default () => {
|
|||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then(() => navigate("/teams"));
|
||||
}).then(() => {
|
||||
setUser({
|
||||
...user,
|
||||
team: null,
|
||||
});
|
||||
navigate("/teams");
|
||||
}).catch(setError);
|
||||
};
|
||||
|
||||
return (<>
|
||||
|
@ -58,7 +103,8 @@ export default () => {
|
|||
<p>Создана: {team.createdAt}</p>
|
||||
|
||||
{!member && !inOtherTeam && !request ? (<Button onClick={sendRequest}>Отправить заявку в команду</Button>) : null}
|
||||
{request ? (<p>Заявка в команду рассматривается</p>) : null}
|
||||
{request ? (<Alert variant="success">Заявка в команду отправлена</Alert>) : null}
|
||||
{error ? (<Alert variant="danger">{error}</Alert>) : null}
|
||||
{member && !isCaptain ? (<Button onClick={leaveTeam}>Выйти из команды</Button>) : null}
|
||||
|
||||
<h2>Участники</h2>
|
||||
|
@ -79,15 +125,51 @@ export default () => {
|
|||
<td>{tm.createdAt}</td>
|
||||
<td>
|
||||
{
|
||||
isCaptain && tm.user.id!=user.id
|
||||
? (<Button variant="outline-danger">Выгнать</Button>)
|
||||
: null
|
||||
isCaptain && tm.user.id != user.id
|
||||
? (<Button variant="outline-danger" onClick={() => removeMember(tm.user.id)}>Выгнать</Button>)
|
||||
: null
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
{isCaptain && (team.members.length == 1)?<Button variant="outline-danger" onClick={deleteTeam}>Удалить команду</Button>:null}
|
||||
{isCaptain
|
||||
? (<>
|
||||
<h2>Заявки</h2>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Имя пользователя</th>
|
||||
<th>Дата заявки</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{team.requests.map(tm => (
|
||||
<tr key={tm.user.id}>
|
||||
<td>{tm.user.username}</td>
|
||||
<td>{tm.createdAt}</td>
|
||||
<td>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="outline-success"
|
||||
onClick={() => approveRequest(tm.user.id, true)}>
|
||||
Принять
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
onClick={() => approveRequest(tm.user.id, false)}>
|
||||
Отказать
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>)
|
||||
: null}
|
||||
{isCaptain && (team.members.length == 1) ? <Button variant="outline-danger" onClick={deleteTeam}>Удалить команду</Button> : null}
|
||||
</>);
|
||||
}
|
|
@ -2,8 +2,10 @@ import { useState } from "react";
|
|||
import { Form, Button, Row, Col } from "react-bootstrap";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ajax } from "../utils/fetch";
|
||||
import { UserProvider } from "../store/user";
|
||||
|
||||
export default () => {
|
||||
const {user, setUser} = UserProvider.useContainer();
|
||||
const [name, setName] = useState("");
|
||||
const [error, setError] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
@ -18,7 +20,14 @@ export default () => {
|
|||
},
|
||||
body: JSON.stringify({ name })
|
||||
}).then((team) => {
|
||||
navigate(`/teams/${team.id}`)
|
||||
setUser({
|
||||
...user,
|
||||
team: {
|
||||
role: 1,
|
||||
...team,
|
||||
}
|
||||
});
|
||||
navigate(`/teams/${team.id}`);
|
||||
}).catch(e => setError(e.message))
|
||||
}
|
||||
return (<>
|
||||
|
|
24
frontend/src/utils/roles.js
Normal file
24
frontend/src/utils/roles.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { UserProvider } from "../store/user"
|
||||
|
||||
const roleHierarchy = {
|
||||
"user": {
|
||||
"user": true
|
||||
},
|
||||
"creator": {
|
||||
"user": true,
|
||||
"creator": true,
|
||||
},
|
||||
"admin": {
|
||||
"user": true,
|
||||
"creator": true,
|
||||
"admin": true,
|
||||
}
|
||||
}
|
||||
|
||||
export const useRole = () => {
|
||||
const { user } = UserProvider.useContainer();
|
||||
|
||||
return {
|
||||
hasRole: (role) => user && !!roleHierarchy[user.role][role]
|
||||
}
|
||||
}
|
7
go.mod
7
go.mod
|
@ -5,7 +5,6 @@ go 1.21.3
|
|||
require (
|
||||
github.com/dimuska139/go-email-normalizer v1.2.1
|
||||
github.com/getkin/kin-openapi v0.120.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/labstack/echo-contrib v0.15.0
|
||||
github.com/labstack/echo/v4 v4.11.2
|
||||
|
@ -17,6 +16,7 @@ require (
|
|||
|
||||
require (
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/deepmap/oapi-codegen/v2 v2.0.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
|
@ -29,6 +29,9 @@ require (
|
|||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
@ -56,7 +59,7 @@ require (
|
|||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wader/gormstore/v2 v2.0.3
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
|
|
14
go.sum
14
go.sum
|
@ -16,6 +16,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deepmap/oapi-codegen/v2 v2.0.0 h1:3TS7w3r+XnjKFXcbFbc16pTWzfTy0OLPkCsutEHjWDA=
|
||||
github.com/deepmap/oapi-codegen/v2 v2.0.0/go.mod h1:7zR+ZL3WzLeCkr2k8oWTxEa0v8y/F25ane0l6A5UjLA=
|
||||
github.com/dimuska139/go-email-normalizer v1.2.1 h1:pJNZnU7uS9MRoYqpoir05B+bCYXrS9sPGE4G1o9EDA8=
|
||||
github.com/dimuska139/go-email-normalizer v1.2.1/go.mod h1:fGPWcd/7PSz9aOHusKVYmDk+oKahH/fZTCQ7tTU7e0Y=
|
||||
github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg=
|
||||
|
@ -104,8 +106,6 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
|
|||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
|
@ -235,6 +235,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
|
|||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
@ -263,8 +265,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -285,6 +287,8 @@ golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -302,6 +306,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
|||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
3
main.go
3
main.go
|
@ -67,7 +67,7 @@ func main() {
|
|||
|
||||
// --[ Services ]--
|
||||
userService := service.NewUser(db)
|
||||
teamService := service.NewTeam(db)
|
||||
teamService := service.NewTeam(db, userService)
|
||||
gameService := service.NewGame(db)
|
||||
|
||||
// --[ HTTP server ]--
|
||||
|
@ -146,6 +146,7 @@ func main() {
|
|||
Options: openapi3filter.Options{
|
||||
AuthenticationFunc: authFunc,
|
||||
},
|
||||
SilenceServersWarning: true,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
@ -45,29 +45,28 @@ func (t *Team) GetTeamsTeamID(ctx echo.Context, teamID int) error {
|
|||
return t.getTeamResponse(ctx, teamID)
|
||||
}
|
||||
|
||||
func (t *Team) DeleteTeamsTeamIDMembers(ctx echo.Context, teamID int) error {
|
||||
return t.getTeamResponse(ctx, teamID)
|
||||
}
|
||||
|
||||
func (t *Team) PostTeamsTeamIDMembers(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,
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
user := contextlib.GetUser(ctx)
|
||||
if user.Team != nil {
|
||||
if err := t.TeamService.RemoveMember(ctx.Request().Context(), user.Team.Team, user); err != nil {
|
||||
return err
|
||||
}
|
||||
user.Team = nil
|
||||
}
|
||||
|
||||
if err := t.TeamService.Request(ctx.Request().Context(), team, user); err != nil {
|
||||
return err
|
||||
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)
|
||||
|
@ -124,7 +123,7 @@ func (t *Team) getTeamResponse(ctx echo.Context, teamID int) error {
|
|||
members := make([]api.TeamMember, 0, len(team.Members))
|
||||
for _, tm := range team.Members {
|
||||
members = append(members, api.TeamMember{
|
||||
Role: api.MapRole[tm.Role],
|
||||
Role: api.MapTeamRole[tm.Role],
|
||||
User: api.UserView{
|
||||
Id: int(tm.User.ID),
|
||||
Username: tm.User.Username,
|
||||
|
@ -151,3 +150,62 @@ func (t *Team) getTeamResponse(ctx echo.Context, teamID int) error {
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -107,6 +107,7 @@ func (u *User) GetUser(c echo.Context) error {
|
|||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Team: api.MapUserTeam(user.Team),
|
||||
Role: api.MapUserRole[user.Role],
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"gitrepo.ru/neonxp/nquest/pkg/models"
|
||||
"gorm.io/gorm"
|
||||
|
@ -15,16 +16,40 @@ var (
|
|||
)
|
||||
|
||||
type Team struct {
|
||||
DB *gorm.DB
|
||||
DB *gorm.DB
|
||||
User *User
|
||||
}
|
||||
|
||||
// NewTeam returns new Team.
|
||||
func NewTeam(db *gorm.DB) *Team {
|
||||
func NewTeam(db *gorm.DB, user *User) *Team {
|
||||
return &Team{
|
||||
DB: db,
|
||||
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{}
|
||||
|
||||
|
@ -49,28 +74,6 @@ func (ts *Team) Create(ctx context.Context, name string, user *models.User) (*mo
|
|||
return t, db.Create(t).Error
|
||||
}
|
||||
|
||||
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) 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) {
|
||||
|
@ -83,7 +86,12 @@ func (ts *Team) Delete(ctx context.Context, id uint) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ts *Team) Request(ctx context.Context, team *models.Team, user *models.User) error {
|
||||
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}).
|
||||
|
@ -94,37 +102,108 @@ func (ts *Team) Request(ctx context.Context, team *models.Team, user *models.Use
|
|||
Error
|
||||
}
|
||||
|
||||
func (ts *Team) ApproveMember(ctx context.Context, team *models.Team, user *models.User) error {
|
||||
team.Requests = slices.DeleteFunc(team.Requests, func(tr *models.TeamRequest) bool {
|
||||
return tr.UserID == user.ID
|
||||
})
|
||||
|
||||
team.Members = append(team.Members, &models.TeamMember{
|
||||
Team: team,
|
||||
User: user,
|
||||
Role: models.Member,
|
||||
})
|
||||
|
||||
db := ts.DB.WithContext(ctx)
|
||||
if err := db.Delete(&models.TeamRequest{}, `user_id = ?`, user.ID).Error; err != nil {
|
||||
func (ts *Team) UpdateMembers(
|
||||
ctx context.Context,
|
||||
teamID uint,
|
||||
newMembers []int,
|
||||
) error {
|
||||
team, err := ts.GetByID(ctx, teamID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Save(team).Error
|
||||
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) DeclineMember(ctx context.Context, team *models.Team, user *models.User) error {
|
||||
team.Requests = slices.DeleteFunc(team.Requests, func(tr *models.TeamRequest) bool {
|
||||
return tr.UserID == user.ID
|
||||
})
|
||||
db := ts.DB.WithContext(ctx)
|
||||
if err := db.Delete(&models.TeamRequest{}, `user_id = ?`, user.ID).Error; err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
return db.Session(&gorm.Session{FullSaveAssociations: true}).Updates(team).Error
|
||||
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) RemoveMember(ctx context.Context, team *models.Team, user *models.User) error {
|
||||
return ts.DB.WithContext(ctx).Delete(&models.TeamMember{}, `user_id = ? and team_id = ?`, user.ID, team.ID).Error
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue