diff --git a/codegen.sh b/codegen.sh index 1655bf232..cc1f1a413 100755 --- a/codegen.sh +++ b/codegen.sh @@ -4,10 +4,10 @@ set -e _PWD=$PWD function yellow { - echo -e "\e[33m$@\e[39m" + echo -e "\033[33m$@\033[39m" } function green { - echo -e "\e[32m$@\e[39m" + echo -e "\033[32m$@\033[39m" } function gofmt { @@ -56,7 +56,7 @@ types function database { yellow "> database" - FOLDERS=$(find -type d -wholename '*/schema/mysql') + FOLDERS=$(find . -type d -wholename '*/schema/mysql') for FOLDER in $FOLDERS; do FOLDER=$(dirname $(dirname $FOLDER)) FOLDER=${FOLDER:2} @@ -70,7 +70,7 @@ database function files { yellow "> files" - FOLDERS=$(find -type d -wholename '*/data') + FOLDERS=$(find . -type d -wholename '*/data') for FOLDER in $FOLDERS; do FOLDER=$(dirname $FOLDER) FOLDER=${FOLDER:2} diff --git a/system/docs/README.md b/system/docs/README.md index 070cca557..44e48d802 100644 --- a/system/docs/README.md +++ b/system/docs/README.md @@ -242,6 +242,36 @@ An organisation may have many teams. Teams may have many channels available. Acc | teamID | uint64 | PATH | Source Team ID | N/A | YES | | destination | uint64 | POST | Destination Team ID | N/A | YES | +## Add member to a team + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/teams/{teamID}/memberAdd` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| teamID | uint64 | PATH | Source Team ID | N/A | YES | +| userID | uint64 | POST | User ID | N/A | YES | + +## Remove member from a team + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/teams/{teamID}/memberRemove` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| teamID | uint64 | PATH | Source Team ID | N/A | YES | +| userID | uint64 | POST | User ID | N/A | YES | + diff --git a/system/docs/src/spec.json b/system/docs/src/spec.json index 12b86a10c..e9e1b329f 100644 --- a/system/docs/src/spec.json +++ b/system/docs/src/spec.json @@ -313,6 +313,54 @@ } ] } + }, + { + "name": "memberAdd", + "method": "POST", + "title": "Add member to a team", + "path": "/{teamID}/memberAdd", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "teamID", + "required": true, + "title": "Source Team ID" + } + ], + "post": [ + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } + }, + { + "name": "memberRemove", + "method": "POST", + "title": "Remove member from a team", + "path": "/{teamID}/memberRemove", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "teamID", + "required": true, + "title": "Source Team ID" + } + ], + "post": [ + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } } ] }, @@ -343,4 +391,4 @@ } ] } -] \ No newline at end of file +] diff --git a/system/docs/src/spec/team.json b/system/docs/src/spec/team.json index 5b5c343f0..d8dcf05e8 100644 --- a/system/docs/src/spec/team.json +++ b/system/docs/src/spec/team.json @@ -175,6 +175,54 @@ } ] } + }, + { + "Name": "memberAdd", + "Method": "POST", + "Title": "Add member to a team", + "Path": "/{teamID}/memberAdd", + "Parameters": { + "path": [ + { + "name": "teamID", + "required": true, + "title": "Source Team ID", + "type": "uint64" + } + ], + "post": [ + { + "name": "userID", + "required": true, + "title": "User ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "memberRemove", + "Method": "POST", + "Title": "Remove member from a team", + "Path": "/{teamID}/memberRemove", + "Parameters": { + "path": [ + { + "name": "teamID", + "required": true, + "title": "Source Team ID", + "type": "uint64" + } + ], + "post": [ + { + "name": "userID", + "required": true, + "title": "User ID", + "type": "uint64" + } + ] + } } ] } \ No newline at end of file diff --git a/system/repository/team.go b/system/repository/team.go index 5c76ec46f..11923117c 100644 --- a/system/repository/team.go +++ b/system/repository/team.go @@ -13,15 +13,17 @@ type ( TeamRepository interface { With(ctx context.Context, db *factory.DB) TeamRepository - FindTeamByID(id uint64) (*types.Team, error) - FindTeams(filter *types.TeamFilter) ([]*types.Team, error) - CreateTeam(mod *types.Team) (*types.Team, error) - UpdateTeam(mod *types.Team) (*types.Team, error) - ArchiveTeamByID(id uint64) error - UnarchiveTeamByID(id uint64) error - DeleteTeamByID(id uint64) error - MergeTeamByID(id, targetTeamID uint64) error - MoveTeamByID(id, targetOrganisationID uint64) error + FindByID(id uint64) (*types.Team, error) + Find(filter *types.TeamFilter) ([]*types.Team, error) + Create(mod *types.Team) (*types.Team, error) + Update(mod *types.Team) (*types.Team, error) + ArchiveByID(id uint64) error + UnarchiveByID(id uint64) error + DeleteByID(id uint64) error + MergeByID(id, targetTeamID uint64) error + MoveByID(id, targetOrganisationID uint64) error + MemberAddByID(id, userID uint64) error + MemberRemoveByID(id, userID uint64) error } team struct { @@ -51,14 +53,14 @@ func (r *team) With(ctx context.Context, db *factory.DB) TeamRepository { } } -func (r *team) FindTeamByID(id uint64) (*types.Team, error) { +func (r *team) FindByID(id uint64) (*types.Team, error) { sql := "SELECT * FROM " + r.teams + " WHERE id = ? AND " + sqlTeamScope mod := &types.Team{} return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrTeamNotFound) } -func (r *team) FindTeams(filter *types.TeamFilter) ([]*types.Team, error) { +func (r *team) Find(filter *types.TeamFilter) ([]*types.Team, error) { rval := make([]*types.Team, 0) params := make([]interface{}, 0) @@ -76,35 +78,51 @@ func (r *team) FindTeams(filter *types.TeamFilter) ([]*types.Team, error) { return rval, r.db().Select(&rval, sql, params...) } -func (r *team) CreateTeam(mod *types.Team) (*types.Team, error) { +func (r *team) Create(mod *types.Team) (*types.Team, error) { mod.ID = factory.Sonyflake.NextID() mod.CreatedAt = time.Now() return mod, r.db().Insert(r.teams, mod) } -func (r *team) UpdateTeam(mod *types.Team) (*types.Team, error) { +func (r *team) Update(mod *types.Team) (*types.Team, error) { mod.UpdatedAt = timeNowPtr() return mod, r.db().Replace(r.teams, mod) } -func (r *team) ArchiveTeamByID(id uint64) error { +func (r *team) ArchiveByID(id uint64) error { return r.updateColumnByID(r.teams, "archived_at", time.Now(), id) } -func (r *team) UnarchiveTeamByID(id uint64) error { +func (r *team) UnarchiveByID(id uint64) error { return r.updateColumnByID(r.teams, "archived_at", nil, id) } -func (r *team) DeleteTeamByID(id uint64) error { +func (r *team) DeleteByID(id uint64) error { return r.updateColumnByID(r.teams, "deleted_at", time.Now(), id) } -func (r *team) MergeTeamByID(id, targetTeamID uint64) error { +func (r *team) MergeByID(id, targetTeamID uint64) error { return ErrNotImplemented } -func (r *team) MoveTeamByID(id, targetOrganisationID uint64) error { +func (r *team) MoveByID(id, targetOrganisationID uint64) error { return ErrNotImplemented } + +func (r *team) MemberAddByID(id, userID uint64) error { + mod := &types.TeamMember{ + TeamID: id, + UserId: userID, + } + return r.db().Replace(r.members, mod) +} + +func (r *team) MemberRemoveByID(id, userID uint64) error { + mod := &types.TeamMember{ + TeamID: id, + UserId: userID, + } + return r.db().Delete(r.members, mod, "rel_team", "rel_user") +} diff --git a/system/repository/team_test.go b/system/repository/team_test.go index 69185c7ab..6ace5a1a9 100644 --- a/system/repository/team_test.go +++ b/system/repository/team_test.go @@ -16,47 +16,76 @@ func TestTeam(t *testing.T) { return } - rpo := Team(context.Background(), factory.Database.MustGet()) + userRepo := User(context.Background(), factory.Database.MustGet()) + user := &types.User{ + Name: "John Doe", + Username: "johndoe", + } + user.GeneratePassword("johndoe") + + { + u1, err := userRepo.Create(user) + assert(t, err == nil, "User.Create error: %v", err) + assert(t, user.ID == u1.ID, "Changes were not stored") + } + + teamRepo := Team(context.Background(), factory.Database.MustGet()) team := &types.Team{ Name: "Test team v1", } { - t1, err := rpo.CreateTeam(team) - assert(t, err == nil, "CreateTeam error: %v", err) + t1, err := teamRepo.Create(team) + assert(t, err == nil, "Team.Create error: %v", err) assert(t, team.Name == t1.Name, "Changes were not stored") } + { team.Name = "Test team v2" - t1, err := rpo.UpdateTeam(team) - assert(t, err == nil, "UpdateTeam error: %v", err) + t1, err := teamRepo.Update(team) + assert(t, err == nil, "Team.Update error: %v", err) assert(t, team.Name == t1.Name, "Changes were not stored") } { - t1, err := rpo.FindTeamByID(team.ID) - assert(t, err == nil, "FindTeamByID error: %v", err) + t1, err := teamRepo.FindByID(team.ID) + assert(t, err == nil, "Team.FindByID error: %v", err) assert(t, team.Name == t1.Name, "Changes were not stored") } { - aa, err := rpo.FindTeams(&types.TeamFilter{Query: team.Name}) - assert(t, err == nil, "FindTeams error: %v", err) + aa, err := teamRepo.Find(&types.TeamFilter{Query: team.Name}) + assert(t, err == nil, "Team.Find error: %v", err) assert(t, len(aa) > 0, "No results found") } { - err := rpo.ArchiveTeamByID(team.ID) - assert(t, err == nil, "ArchiveTeamByID error: %v", err) + err := teamRepo.ArchiveByID(team.ID) + assert(t, err == nil, "Team.ArchiveByID error: %v", err) } { - err := rpo.UnarchiveTeamByID(team.ID) - assert(t, err == nil, "UnarchiveTeamByID error: %v", err) + err := teamRepo.UnarchiveByID(team.ID) + assert(t, err == nil, "Team.UnarchiveByID error: %v", err) } { - err := rpo.DeleteTeamByID(team.ID) - assert(t, err == nil, "DeleteTeamByID error: %v", err) + err := teamRepo.MemberAddByID(team.ID, user.ID) + assert(t, err == nil, "Team.MemberAddByID error: %v", err) + } + + { + err := teamRepo.MemberRemoveByID(team.ID, user.ID) + assert(t, err == nil, "Team.MemberRemoveByID error: %v", err) + } + + { + err := teamRepo.DeleteByID(team.ID) + assert(t, err == nil, "Team.DeleteByID error: %v", err) + } + + { + err := userRepo.DeleteByID(user.ID) + assert(t, err == nil, "User.DeleteByID error: %v", err) } } diff --git a/system/rest/handlers/team.go b/system/rest/handlers/team.go index ebb46fe4f..1afc0751b 100644 --- a/system/rest/handlers/team.go +++ b/system/rest/handlers/team.go @@ -35,18 +35,22 @@ type TeamAPI interface { Archive(context.Context, *request.TeamArchive) (interface{}, error) Move(context.Context, *request.TeamMove) (interface{}, error) Merge(context.Context, *request.TeamMerge) (interface{}, error) + MemberAdd(context.Context, *request.TeamMemberAdd) (interface{}, error) + MemberRemove(context.Context, *request.TeamMemberRemove) (interface{}, error) } // HTTP API interface type Team struct { - List func(http.ResponseWriter, *http.Request) - Create func(http.ResponseWriter, *http.Request) - Edit func(http.ResponseWriter, *http.Request) - Read func(http.ResponseWriter, *http.Request) - Remove func(http.ResponseWriter, *http.Request) - Archive func(http.ResponseWriter, *http.Request) - Move func(http.ResponseWriter, *http.Request) - Merge func(http.ResponseWriter, *http.Request) + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Edit func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Remove func(http.ResponseWriter, *http.Request) + Archive func(http.ResponseWriter, *http.Request) + Move func(http.ResponseWriter, *http.Request) + Merge func(http.ResponseWriter, *http.Request) + MemberAdd func(http.ResponseWriter, *http.Request) + MemberRemove func(http.ResponseWriter, *http.Request) } func NewTeam(th TeamAPI) *Team { @@ -107,6 +111,20 @@ func NewTeam(th TeamAPI) *Team { return th.Merge(r.Context(), params) }) }, + MemberAdd: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewTeamMemberAdd() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return th.MemberAdd(r.Context(), params) + }) + }, + MemberRemove: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewTeamMemberRemove() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return th.MemberRemove(r.Context(), params) + }) + }, } } @@ -122,6 +140,8 @@ func (th *Team) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http r.Post("/{teamID}/archive", th.Archive) r.Post("/{teamID}/move", th.Move) r.Post("/{teamID}/merge", th.Merge) + r.Post("/{teamID}/memberAdd", th.MemberAdd) + r.Post("/{teamID}/memberRemove", th.MemberRemove) }) }) } diff --git a/system/rest/request/team.go b/system/rest/request/team.go index 13f57d8c3..cfd402cd7 100644 --- a/system/rest/request/team.go +++ b/system/rest/request/team.go @@ -398,3 +398,99 @@ func (t *TeamMerge) Fill(r *http.Request) (err error) { } var _ RequestFiller = NewTeamMerge() + +// Team memberAdd request parameters +type TeamMemberAdd struct { + TeamID uint64 `json:",string"` + UserID uint64 `json:",string"` +} + +func NewTeamMemberAdd() *TeamMemberAdd { + return &TeamMemberAdd{} +} + +func (t *TeamMemberAdd) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(t) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + t.TeamID = parseUInt64(chi.URLParam(r, "teamID")) + if val, ok := post["userID"]; ok { + + t.UserID = parseUInt64(val) + } + + return err +} + +var _ RequestFiller = NewTeamMemberAdd() + +// Team memberRemove request parameters +type TeamMemberRemove struct { + TeamID uint64 `json:",string"` + UserID uint64 `json:",string"` +} + +func NewTeamMemberRemove() *TeamMemberRemove { + return &TeamMemberRemove{} +} + +func (t *TeamMemberRemove) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(t) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + t.TeamID = parseUInt64(chi.URLParam(r, "teamID")) + if val, ok := post["userID"]; ok { + + t.UserID = parseUInt64(val) + } + + return err +} + +var _ RequestFiller = NewTeamMemberRemove() diff --git a/system/rest/team.go b/system/rest/team.go index 1e90b93e7..fa54fe925 100644 --- a/system/rest/team.go +++ b/system/rest/team.go @@ -66,3 +66,11 @@ func (ctrl *Team) Merge(ctx context.Context, r *request.TeamMerge) (interface{}, func (ctrl *Team) Move(ctx context.Context, r *request.TeamMove) (interface{}, error) { return nil, ctrl.svc.team.With(ctx).Move(r.TeamID, r.OrganisationID) } + +func (ctrl *Team) MemberAdd(ctx context.Context, r *request.TeamMemberAdd) (interface{}, error) { + return nil, ctrl.svc.team.With(ctx).Merge(r.TeamID, r.UserID) +} + +func (ctrl *Team) MemberRemove(ctx context.Context, r *request.TeamMemberRemove) (interface{}, error) { + return nil, ctrl.svc.team.With(ctx).Move(r.TeamID, r.UserID) +} diff --git a/system/service/team.go b/system/service/team.go index 3456d6737..b5671b422 100644 --- a/system/service/team.go +++ b/system/service/team.go @@ -31,6 +31,9 @@ type ( Archive(ID uint64) error Unarchive(ID uint64) error Delete(ID uint64) error + + MemberAdd(teamID, userID uint64) error + MemberRemove(teamID, userID uint64) error } ) @@ -49,56 +52,66 @@ func (svc *team) With(ctx context.Context) TeamService { func (svc *team) FindByID(id uint64) (*types.Team, error) { // @todo: permission check if current user has access to this team - return svc.team.FindTeamByID(id) + return svc.team.FindByID(id) } func (svc *team) Find(filter *types.TeamFilter) ([]*types.Team, error) { // @todo: permission check to return only teams that current user has access to - return svc.team.FindTeams(filter) + return svc.team.Find(filter) } func (svc *team) Create(mod *types.Team) (*types.Team, error) { // @todo: permission check if current user can add/edit team - return svc.team.CreateTeam(mod) + return svc.team.Create(mod) } func (svc *team) Update(mod *types.Team) (*types.Team, error) { // @todo: permission check if current user can add/edit team // @todo: make sure archived & deleted entries can not be edited - return svc.team.UpdateTeam(mod) + return svc.team.Update(mod) } func (svc *team) Delete(id uint64) error { // @todo: make history unavailable // @todo: notify users that team has been removed (remove from web UI) // @todo: permissions check if current user can remove team - return svc.team.DeleteTeamByID(id) + return svc.team.DeleteByID(id) } func (svc *team) Archive(id uint64) error { // @todo: make history unavailable // @todo: notify users that team has been removed (remove from web UI) // @todo: permissions check if current user can remove team - return svc.team.ArchiveTeamByID(id) + return svc.team.ArchiveByID(id) } func (svc *team) Unarchive(id uint64) error { // @todo: permissions check if current user can unarchive team // @todo: make history accessible // @todo: notify users that team has been unarchived - return svc.team.UnarchiveTeamByID(id) + return svc.team.UnarchiveByID(id) } func (svc *team) Merge(id, targetTeamID uint64) error { // @todo: permission check if current user can merge team - return svc.team.MergeTeamByID(id, targetTeamID) + return svc.team.MergeByID(id, targetTeamID) } func (svc *team) Move(id, targetOrganisationID uint64) error { // @todo: permission check if current user can move team to another organisation - return svc.team.MoveTeamByID(id, targetOrganisationID) + return svc.team.MoveByID(id, targetOrganisationID) +} + +func (svc *team) MemberAdd(id, userID uint64) error { + // @todo: permission check if current user can add user in to a team + return svc.team.MemberAddByID(id, userID) +} + +func (svc *team) MemberRemove(id, userID uint64) error { + // @todo: permission check if current user can remove user from a team + return svc.team.MemberRemoveByID(id, userID) } var _ TeamService = &team{} diff --git a/system/types/team_members.go b/system/types/team_members.go new file mode 100644 index 000000000..a2ef53aec --- /dev/null +++ b/system/types/team_members.go @@ -0,0 +1,12 @@ +package types + +type ( + TeamMember struct { + TeamID uint64 `db:"rel_team"` + UserId uint64 `db:"rel_user"` + } + + TeamMemberFilter struct { + Query string + } +)