From d0fce2b2fe5f3d43b1b4d1a7144be280a8f17c58 Mon Sep 17 00:00:00 2001
From: NeonXP
Date: Sun, 12 Nov 2023 23:22:58 +0300
Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6?=
=?UTF-8?q?=D0=BA=D0=B0=20=D1=80=D0=BE=D0=BB=D0=B5=D0=B9=20=D0=BF=D0=BE?=
=?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 1 +
Makefile | 9 +-
api/doc.go | 4 +
api/mapper.go | 22 +++-
api/openapi.yaml | 62 +++++++++-
api/server.go | 86 +++++++++++---
api/types.go | 30 ++++-
config.go | 7 --
frontend/src/components/Layout.jsx | 50 ++++----
frontend/src/pages/Team.jsx | 114 ++++++++++++++++---
frontend/src/pages/TeamNew.jsx | 11 +-
frontend/src/utils/roles.js | 24 ++++
go.mod | 7 +-
go.sum | 14 ++-
main.go | 3 +-
pkg/controller/team.go | 96 ++++++++++++----
pkg/controller/user.go | 1 +
pkg/service/team.go | 177 +++++++++++++++++++++--------
18 files changed, 570 insertions(+), 148 deletions(-)
create mode 100644 .gitignore
create mode 100644 api/doc.go
create mode 100644 frontend/src/utils/roles.js
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d1b811b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.sql
diff --git a/Makefile b/Makefile
index 0390f4c..6bbdd19 100644
--- a/Makefile
+++ b/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
\ No newline at end of file
+ cd frontend; npm run dev
\ No newline at end of file
diff --git a/api/doc.go b/api/doc.go
new file mode 100644
index 0000000..07f36fe
--- /dev/null
+++ b/api/doc.go
@@ -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
diff --git a/api/mapper.go b/api/mapper.go
index 146348f..b53ecb6 100644
--- a/api/mapper.go
+++ b/api/mapper.go
@@ -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],
}
}
diff --git a/api/openapi.yaml b/api/openapi.yaml
index 005f374..4a7dad0 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -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:
diff --git a/api/server.go b/api/server.go
index 9d7d281..1f84b91 100644
--- a/api/server.go
+++ b/api/server.go
@@ -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{
- "H4sIAAAAAAAC/8xYTW+rOBT9KyPPLFFIP1bsOqpUVdNKM5m8t4mycOE2dV+wefalVVTx35+usQkUSCBN",
- "07cKsa/tc8798IU3Fqs0UxIkGha9MQ0/czD4t0oE2IG1WglJD7GSCBLpkWfZWsQchZLhs1F22sRPkHJ6",
- "yrTKQKNbDykXa3rATQYsYga1kCtWBCzjxrwqnXRMFoEFIjQkLFq4PWorloFfoR6eIUZWNJegzsEOrIRB",
- "0KeGv50875zNDWjJU9jPvLIM2iLUTxkkiB0xmZLGcdNa6Zkb+YBGsUrqVIREWIEmoikYw1cDeNottvbd",
- "dBIwsRYZYWIRo/1XPIU7YfAgEgIhtfj/0vDIIvZnuE2FsDQzIZ3wXcArneYgca35pg8RAk+PIakGjpBc",
- "YWf8iKRP7vQBtBlMjsDe2zVtegHridDA14hx58zKRR06NiNBUGC7kPd8akcGNWmGRgkdbz43TOiIMWFC",
- "eX2EMOmvTn0hQkD3kSFwc7IbVaqs41r1apiPisBRtKyqnGvxbawbwdog19iTTDY6juDogKHA9VChStum",
- "ElucHlVbvXLGJe3IsqFVCW+I72dk6/w/ZE2pScftxdy5uxPXF86yRozkdVKM3YG5G2Ccaw0S5y733PyD",
- "UmvgcmBBb0+OSEuXkrvZVWnfYtcHr/+GGB1pO1DbzXYBnrnTQOYpLS5VI748Qy5kbXEzaLp92cf20FLY",
- "UQIDZiDOtcDN/6SGb6PUDwFXOT5ZFFQZyyEvRcQMGFNWCl97MvEPuFtGyEdlsZVliMn/bDIF7AW0KSvt",
- "2WQ6mRIXlYHkmWARu5icTaa2l8QnC8O2PGUdBhvOpI29jm4TFrEbwBtr8K6ZPJ9O+zxe2YWtfq0uBYsW",
- "S/ofVhW5D8DcGhwCoN0KFAG7nF7sX9lslq3gmTId+P5VpgbQv01tPnDLDwu6vlhrtf2DhWpqdHmARpU3",
- "wzf6ub0uylt8DQht5a7tuNVubq1tWGqeAtoiuHBZQaG6zQn0ps23naAm5/tcLpYtSS5b7QX7SGjsjNyT",
- "k/tCf4e1K2yw3++rvv8TFTosM6mq31537vqlmba3Gp1Q2a+LPd8J9qXfN9fsjYbYeF87uC54iGH1Ua3f",
- "aQT1zpq1grX71Np3O7d/cRym0/FMOy51z1vlOIg42Q2p0iaPYzDmD7f1sRHXPyHuxjzzlgf4qzrl93FZ",
- "c6DZnS6WlOgG9IsvH7les4iF1FIWy+JXAAAA//+dkMP2VRYAAA==",
+ "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",
+ "tlEaAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/api/types.go b/api/types.go
index 6d7d3be..30ba941 100644
--- a/api/types.go
+++ b/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
diff --git a/config.go b/config.go
index 8794c6b..89b858b 100644
--- a/config.go
+++ b/config.go
@@ -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)
}
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx
index 476c5ec..c9e0396 100644
--- a/frontend/src/components/Layout.jsx
+++ b/frontend/src/components/Layout.jsx
@@ -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 () => {
Игры
-
- Команды
-
+ {hasRole("user") ? (<>
+
+ Команды
+
+ {hasRole("creator") ? (
+
+ Админка
+
+ ) : null}
+ >) : null}
- {user ? (
- <>
- {user.username}
- {user.team ? (
- <>({user.team.name})>
- ) : (
- <>(без команды)>
- )}
-
- >
- ) : (
-
- Вход
- Регистрация
-
- )}
-
+ {user ? (
+ <>
+ {user.username}
+ {user.team ? (
+ <>({user.team.name})>
+ ) : (
+ <>(без команды)>
+ )}
+
+ >
+ ) : (
+
+ Вход
+ Регистрация
+
+ )}
+
diff --git a/frontend/src/pages/Team.jsx b/frontend/src/pages/Team.jsx
index 14baa69..2f892aa 100644
--- a/frontend/src/pages/Team.jsx
+++ b/frontend/src/pages/Team.jsx
@@ -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 () => {
Создана: {team.createdAt}
{!member && !inOtherTeam && !request ? () : null}
- {request ? (Заявка в команду рассматривается
) : null}
+ {request ? (Заявка в команду отправлена) : null}
+ {error ? ({error}) : null}
{member && !isCaptain ? () : null}
Участники
@@ -79,15 +125,51 @@ export default () => {
{tm.createdAt} |
{
- isCaptain && tm.user.id!=user.id
- ? ()
- : null
+ isCaptain && tm.user.id != user.id
+ ? ()
+ : null
}
|
))}
- {isCaptain && (team.members.length == 1)?:null}
+ {isCaptain
+ ? (<>
+ Заявки
+
+
+
+ Имя пользователя |
+ Дата заявки |
+ |
+
+
+
+ {team.requests.map(tm => (
+
+ {tm.user.username} |
+ {tm.createdAt} |
+
+
+
+
+
+ |
+
+ ))}
+
+
+ >)
+ : null}
+ {isCaptain && (team.members.length == 1) ? : null}
>);
}
\ No newline at end of file
diff --git a/frontend/src/pages/TeamNew.jsx b/frontend/src/pages/TeamNew.jsx
index 673d14e..7498c5c 100644
--- a/frontend/src/pages/TeamNew.jsx
+++ b/frontend/src/pages/TeamNew.jsx
@@ -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 (<>
diff --git a/frontend/src/utils/roles.js b/frontend/src/utils/roles.js
new file mode 100644
index 0000000..41ee59e
--- /dev/null
+++ b/frontend/src/utils/roles.js
@@ -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]
+ }
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index b0ae6de..5c41630 100644
--- a/go.mod
+++ b/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
diff --git a/go.sum b/go.sum
index 9b3142a..764f1cc 100644
--- a/go.sum
+++ b/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=
diff --git a/main.go b/main.go
index 180d93b..c52f668 100644
--- a/main.go
+++ b/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,
},
),
)
diff --git a/pkg/controller/team.go b/pkg/controller/team.go
index 4dc3423..b187163 100644
--- a/pkg/controller/team.go
+++ b/pkg/controller/team.go
@@ -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)
+}
diff --git a/pkg/controller/user.go b/pkg/controller/user.go
index 45a209a..38dee48 100644
--- a/pkg/controller/user.go
+++ b/pkg/controller/user.go
@@ -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],
})
}
diff --git a/pkg/service/team.go b/pkg/service/team.go
index bcf1e35..d1747cc 100644
--- a/pkg/service/team.go
+++ b/pkg/service/team.go
@@ -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
}