mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Add team member invite by email (#20307)
Allows to add (not registered) team members by email. related #5353 Invite by mail:  Pending invitations:  Email:  Join form:  Co-authored-by: Jack Hay <jjphay@gmail.com>
This commit is contained in:
		@@ -417,6 +417,8 @@ var migrations = []Migration{
 | 
				
			|||||||
	NewMigration("Conan and generic packages do not need to be semantically versioned", fixPackageSemverField),
 | 
						NewMigration("Conan and generic packages do not need to be semantically versioned", fixPackageSemverField),
 | 
				
			||||||
	// v227 -> v228
 | 
						// v227 -> v228
 | 
				
			||||||
	NewMigration("Create key/value table for system settings", createSystemSettingsTable),
 | 
						NewMigration("Create key/value table for system settings", createSystemSettingsTable),
 | 
				
			||||||
 | 
						// v228 -> v229
 | 
				
			||||||
 | 
						NewMigration("Add TeamInvite table", addTeamInviteTable),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetCurrentDBVersion returns the current db version
 | 
					// GetCurrentDBVersion returns the current db version
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										26
									
								
								models/migrations/v228.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								models/migrations/v228.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/xorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func addTeamInviteTable(x *xorm.Engine) error {
 | 
				
			||||||
 | 
						type TeamInvite struct {
 | 
				
			||||||
 | 
							ID          int64              `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
							Token       string             `xorm:"UNIQUE(token) INDEX NOT NULL DEFAULT ''"`
 | 
				
			||||||
 | 
							InviterID   int64              `xorm:"NOT NULL DEFAULT 0"`
 | 
				
			||||||
 | 
							OrgID       int64              `xorm:"INDEX NOT NULL DEFAULT 0"`
 | 
				
			||||||
 | 
							TeamID      int64              `xorm:"UNIQUE(team_mail) INDEX NOT NULL DEFAULT 0"`
 | 
				
			||||||
 | 
							Email       string             `xorm:"UNIQUE(team_mail) NOT NULL DEFAULT ''"`
 | 
				
			||||||
 | 
							CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
 | 
				
			||||||
 | 
							UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return x.Sync2(new(TeamInvite))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -431,25 +431,15 @@ func DeleteTeam(t *organization.Team) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Delete team-user.
 | 
						if err := db.DeleteBeans(ctx,
 | 
				
			||||||
	if _, err := sess.
 | 
							&organization.Team{ID: t.ID},
 | 
				
			||||||
		Where("org_id=?", t.OrgID).
 | 
							&organization.TeamUser{OrgID: t.OrgID, TeamID: t.ID},
 | 
				
			||||||
		Where("team_id=?", t.ID).
 | 
							&organization.TeamUnit{TeamID: t.ID},
 | 
				
			||||||
		Delete(new(organization.TeamUser)); err != nil {
 | 
							&organization.TeamInvite{TeamID: t.ID},
 | 
				
			||||||
 | 
						); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Delete team-unit.
 | 
					 | 
				
			||||||
	if _, err := sess.
 | 
					 | 
				
			||||||
		Where("team_id=?", t.ID).
 | 
					 | 
				
			||||||
		Delete(new(organization.TeamUnit)); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Delete team.
 | 
					 | 
				
			||||||
	if _, err := sess.ID(t.ID).Delete(new(organization.Team)); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	// Update organization number of teams.
 | 
						// Update organization number of teams.
 | 
				
			||||||
	if _, err := sess.Exec("UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil {
 | 
						if _, err := sess.Exec("UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -370,8 +370,9 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
 | 
				
			|||||||
		&OrgUser{OrgID: org.ID},
 | 
							&OrgUser{OrgID: org.ID},
 | 
				
			||||||
		&TeamUser{OrgID: org.ID},
 | 
							&TeamUser{OrgID: org.ID},
 | 
				
			||||||
		&TeamUnit{OrgID: org.ID},
 | 
							&TeamUnit{OrgID: org.ID},
 | 
				
			||||||
 | 
							&TeamInvite{OrgID: org.ID},
 | 
				
			||||||
	); err != nil {
 | 
						); err != nil {
 | 
				
			||||||
		return fmt.Errorf("deleteBeans: %v", err)
 | 
							return fmt.Errorf("DeleteBeans: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, err := db.GetEngine(ctx).ID(org.ID).Delete(new(user_model.User)); err != nil {
 | 
						if _, err := db.GetEngine(ctx).ID(org.ID).Delete(new(user_model.User)); err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -94,6 +94,7 @@ func init() {
 | 
				
			|||||||
	db.RegisterModel(new(TeamUser))
 | 
						db.RegisterModel(new(TeamUser))
 | 
				
			||||||
	db.RegisterModel(new(TeamRepo))
 | 
						db.RegisterModel(new(TeamRepo))
 | 
				
			||||||
	db.RegisterModel(new(TeamUnit))
 | 
						db.RegisterModel(new(TeamUnit))
 | 
				
			||||||
 | 
						db.RegisterModel(new(TeamInvite))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SearchTeamOptions holds the search options
 | 
					// SearchTeamOptions holds the search options
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										162
									
								
								models/organization/team_invite.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								models/organization/team_invite.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,162 @@
 | 
				
			|||||||
 | 
					// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package organization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/builder"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ErrTeamInviteAlreadyExist struct {
 | 
				
			||||||
 | 
						TeamID int64
 | 
				
			||||||
 | 
						Email  string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func IsErrTeamInviteAlreadyExist(err error) bool {
 | 
				
			||||||
 | 
						_, ok := err.(ErrTeamInviteAlreadyExist)
 | 
				
			||||||
 | 
						return ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (err ErrTeamInviteAlreadyExist) Error() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("team invite already exists [team_id: %d, email: %s]", err.TeamID, err.Email)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (err ErrTeamInviteAlreadyExist) Unwrap() error {
 | 
				
			||||||
 | 
						return util.ErrAlreadyExist
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ErrTeamInviteNotFound struct {
 | 
				
			||||||
 | 
						Token string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func IsErrTeamInviteNotFound(err error) bool {
 | 
				
			||||||
 | 
						_, ok := err.(ErrTeamInviteNotFound)
 | 
				
			||||||
 | 
						return ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (err ErrTeamInviteNotFound) Error() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("team invite was not found [token: %s]", err.Token)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (err ErrTeamInviteNotFound) Unwrap() error {
 | 
				
			||||||
 | 
						return util.ErrNotExist
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ErrUserEmailAlreadyAdded represents a "user by email already added to team" error.
 | 
				
			||||||
 | 
					type ErrUserEmailAlreadyAdded struct {
 | 
				
			||||||
 | 
						Email string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsErrUserEmailAlreadyAdded checks if an error is a ErrUserEmailAlreadyAdded.
 | 
				
			||||||
 | 
					func IsErrUserEmailAlreadyAdded(err error) bool {
 | 
				
			||||||
 | 
						_, ok := err.(ErrUserEmailAlreadyAdded)
 | 
				
			||||||
 | 
						return ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (err ErrUserEmailAlreadyAdded) Error() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("user with email already added [email: %s]", err.Email)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (err ErrUserEmailAlreadyAdded) Unwrap() error {
 | 
				
			||||||
 | 
						return util.ErrAlreadyExist
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TeamInvite represents an invite to a team
 | 
				
			||||||
 | 
					type TeamInvite struct {
 | 
				
			||||||
 | 
						ID          int64              `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
						Token       string             `xorm:"UNIQUE(token) INDEX NOT NULL DEFAULT ''"`
 | 
				
			||||||
 | 
						InviterID   int64              `xorm:"NOT NULL DEFAULT 0"`
 | 
				
			||||||
 | 
						OrgID       int64              `xorm:"INDEX NOT NULL DEFAULT 0"`
 | 
				
			||||||
 | 
						TeamID      int64              `xorm:"UNIQUE(team_mail) INDEX NOT NULL DEFAULT 0"`
 | 
				
			||||||
 | 
						Email       string             `xorm:"UNIQUE(team_mail) NOT NULL DEFAULT ''"`
 | 
				
			||||||
 | 
						CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
 | 
				
			||||||
 | 
						UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func CreateTeamInvite(ctx context.Context, doer *user_model.User, team *Team, email string) (*TeamInvite, error) {
 | 
				
			||||||
 | 
						has, err := db.GetEngine(ctx).Exist(&TeamInvite{
 | 
				
			||||||
 | 
							TeamID: team.ID,
 | 
				
			||||||
 | 
							Email:  email,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if has {
 | 
				
			||||||
 | 
							return nil, ErrTeamInviteAlreadyExist{
 | 
				
			||||||
 | 
								TeamID: team.ID,
 | 
				
			||||||
 | 
								Email:  email,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// check if the user is already a team member by email
 | 
				
			||||||
 | 
						exist, err := db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Where(builder.Eq{
 | 
				
			||||||
 | 
								"team_user.org_id":  team.OrgID,
 | 
				
			||||||
 | 
								"team_user.team_id": team.ID,
 | 
				
			||||||
 | 
								"`user`.email":      email,
 | 
				
			||||||
 | 
							}).
 | 
				
			||||||
 | 
							Join("INNER", "`user`", "`user`.id = team_user.uid").
 | 
				
			||||||
 | 
							Table("team_user").
 | 
				
			||||||
 | 
							Exist()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if exist {
 | 
				
			||||||
 | 
							return nil, ErrUserEmailAlreadyAdded{
 | 
				
			||||||
 | 
								Email: email,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						token, err := util.CryptoRandomString(25)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						invite := &TeamInvite{
 | 
				
			||||||
 | 
							Token:     token,
 | 
				
			||||||
 | 
							InviterID: doer.ID,
 | 
				
			||||||
 | 
							OrgID:     team.OrgID,
 | 
				
			||||||
 | 
							TeamID:    team.ID,
 | 
				
			||||||
 | 
							Email:     email,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return invite, db.Insert(ctx, invite)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func RemoveInviteByID(ctx context.Context, inviteID, teamID int64) error {
 | 
				
			||||||
 | 
						_, err := db.DeleteByBean(ctx, &TeamInvite{
 | 
				
			||||||
 | 
							ID:     inviteID,
 | 
				
			||||||
 | 
							TeamID: teamID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetInvitesByTeamID(ctx context.Context, teamID int64) ([]*TeamInvite, error) {
 | 
				
			||||||
 | 
						invites := make([]*TeamInvite, 0, 10)
 | 
				
			||||||
 | 
						return invites, db.GetEngine(ctx).
 | 
				
			||||||
 | 
							Where("team_id=?", teamID).
 | 
				
			||||||
 | 
							Find(&invites)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetInviteByToken(ctx context.Context, token string) (*TeamInvite, error) {
 | 
				
			||||||
 | 
						invite := &TeamInvite{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						has, err := db.GetEngine(ctx).Where("token=?", token).Get(invite)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !has {
 | 
				
			||||||
 | 
							return nil, ErrTeamInviteNotFound{Token: token}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return invite, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										49
									
								
								models/organization/team_invite_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								models/organization/team_invite_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package organization_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/organization"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestTeamInvite(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("MailExistsInTeam", func(t *testing.T) {
 | 
				
			||||||
 | 
							user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// user 2 already added to team 2, should result in error
 | 
				
			||||||
 | 
							_, err := organization.CreateTeamInvite(db.DefaultContext, user2, team, user2.Email)
 | 
				
			||||||
 | 
							assert.Error(t, err)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("CreateAndRemove", func(t *testing.T) {
 | 
				
			||||||
 | 
							user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							invite, err := organization.CreateTeamInvite(db.DefaultContext, user1, team, "user3@example.com")
 | 
				
			||||||
 | 
							assert.NotNil(t, invite)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Shouldn't allow duplicate invite
 | 
				
			||||||
 | 
							_, err = organization.CreateTeamInvite(db.DefaultContext, user1, team, "user3@example.com")
 | 
				
			||||||
 | 
							assert.Error(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// should remove invite
 | 
				
			||||||
 | 
							assert.NoError(t, organization.RemoveInviteByID(db.DefaultContext, invite.ID, invite.TeamID))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// invite should not exist
 | 
				
			||||||
 | 
							_, err = organization.GetInviteByToken(db.DefaultContext, invite.Token)
 | 
				
			||||||
 | 
							assert.Error(t, err)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -412,6 +412,11 @@ repo.transfer.body = To accept or reject it visit %s or just ignore it.
 | 
				
			|||||||
repo.collaborator.added.subject = %s added you to %s
 | 
					repo.collaborator.added.subject = %s added you to %s
 | 
				
			||||||
repo.collaborator.added.text = You have been added as a collaborator of repository:
 | 
					repo.collaborator.added.text = You have been added as a collaborator of repository:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					team_invite.subject = %[1]s has invited you to join the %[2]s organization
 | 
				
			||||||
 | 
					team_invite.text_1 = %[1]s has invited you to join team %[2]s in organization %[3]s.
 | 
				
			||||||
 | 
					team_invite.text_2 = Please click the following link to join the team:
 | 
				
			||||||
 | 
					team_invite.text_3 = Note: This invitation was intended for %[1]s. If you were not expecting this invitation, you can ignore this email.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[modal]
 | 
					[modal]
 | 
				
			||||||
yes = Yes
 | 
					yes = Yes
 | 
				
			||||||
no = No
 | 
					no = No
 | 
				
			||||||
@@ -487,6 +492,7 @@ user_not_exist = The user does not exist.
 | 
				
			|||||||
team_not_exist = The team does not exist.
 | 
					team_not_exist = The team does not exist.
 | 
				
			||||||
last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner for an organization.
 | 
					last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner for an organization.
 | 
				
			||||||
cannot_add_org_to_team = An organization cannot be added as a team member.
 | 
					cannot_add_org_to_team = An organization cannot be added as a team member.
 | 
				
			||||||
 | 
					duplicate_invite_to_team = The user was already invited as a team member.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
invalid_ssh_key = Can not verify your SSH key: %s
 | 
					invalid_ssh_key = Can not verify your SSH key: %s
 | 
				
			||||||
invalid_gpg_key = Can not verify your GPG key: %s
 | 
					invalid_gpg_key = Can not verify your GPG key: %s
 | 
				
			||||||
@@ -2402,6 +2408,8 @@ teams.members = Team Members
 | 
				
			|||||||
teams.update_settings = Update Settings
 | 
					teams.update_settings = Update Settings
 | 
				
			||||||
teams.delete_team = Delete Team
 | 
					teams.delete_team = Delete Team
 | 
				
			||||||
teams.add_team_member = Add Team Member
 | 
					teams.add_team_member = Add Team Member
 | 
				
			||||||
 | 
					teams.invite_team_member = Invite to %s
 | 
				
			||||||
 | 
					teams.invite_team_member.list = Pending Invitations
 | 
				
			||||||
teams.delete_team_title = Delete Team
 | 
					teams.delete_team_title = Delete Team
 | 
				
			||||||
teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue?
 | 
					teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue?
 | 
				
			||||||
teams.delete_team_success = The team has been deleted.
 | 
					teams.delete_team_success = The team has been deleted.
 | 
				
			||||||
@@ -2426,6 +2434,9 @@ teams.all_repositories_helper = Team has access to all repositories. Selecting t
 | 
				
			|||||||
teams.all_repositories_read_permission_desc = This team grants <strong>Read</strong> access to <strong>all repositories</strong>: members can view and clone repositories.
 | 
					teams.all_repositories_read_permission_desc = This team grants <strong>Read</strong> access to <strong>all repositories</strong>: members can view and clone repositories.
 | 
				
			||||||
teams.all_repositories_write_permission_desc = This team grants <strong>Write</strong> access to <strong>all repositories</strong>: members can read from and push to repositories.
 | 
					teams.all_repositories_write_permission_desc = This team grants <strong>Write</strong> access to <strong>all repositories</strong>: members can read from and push to repositories.
 | 
				
			||||||
teams.all_repositories_admin_permission_desc = This team grants <strong>Admin</strong> access to <strong>all repositories</strong>: members can read from, push to and add collaborators to repositories.
 | 
					teams.all_repositories_admin_permission_desc = This team grants <strong>Admin</strong> access to <strong>all repositories</strong>: members can read from, push to and add collaborators to repositories.
 | 
				
			||||||
 | 
					teams.invite.title = You've been invited to join team <strong>%s</strong> in organization <strong>%s</strong>.
 | 
				
			||||||
 | 
					teams.invite.by = Invited by %s
 | 
				
			||||||
 | 
					teams.invite.description = Please click the button below to join the team.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[admin]
 | 
					[admin]
 | 
				
			||||||
dashboard = Dashboard
 | 
					dashboard = Dashboard
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
	"code.gitea.io/gitea/models/organization"
 | 
						org_model "code.gitea.io/gitea/models/organization"
 | 
				
			||||||
	"code.gitea.io/gitea/models/perm"
 | 
						"code.gitea.io/gitea/models/perm"
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
	unit_model "code.gitea.io/gitea/models/unit"
 | 
						unit_model "code.gitea.io/gitea/models/unit"
 | 
				
			||||||
@@ -23,9 +23,11 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/convert"
 | 
						"code.gitea.io/gitea/modules/convert"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web"
 | 
						"code.gitea.io/gitea/modules/web"
 | 
				
			||||||
	"code.gitea.io/gitea/routers/utils"
 | 
						"code.gitea.io/gitea/routers/utils"
 | 
				
			||||||
	"code.gitea.io/gitea/services/forms"
 | 
						"code.gitea.io/gitea/services/forms"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/mailer"
 | 
				
			||||||
	org_service "code.gitea.io/gitea/services/org"
 | 
						org_service "code.gitea.io/gitea/services/org"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,6 +40,8 @@ const (
 | 
				
			|||||||
	tplTeamMembers base.TplName = "org/team/members"
 | 
						tplTeamMembers base.TplName = "org/team/members"
 | 
				
			||||||
	// tplTeamRepositories template path for showing team repositories page
 | 
						// tplTeamRepositories template path for showing team repositories page
 | 
				
			||||||
	tplTeamRepositories base.TplName = "org/team/repositories"
 | 
						tplTeamRepositories base.TplName = "org/team/repositories"
 | 
				
			||||||
 | 
						// tplTeamInvite template path for team invites page
 | 
				
			||||||
 | 
						tplTeamInvite base.TplName = "org/team/invite"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Teams render teams list page
 | 
					// Teams render teams list page
 | 
				
			||||||
@@ -59,12 +63,6 @@ func Teams(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// TeamsAction response for join, leave, remove, add operations to team
 | 
					// TeamsAction response for join, leave, remove, add operations to team
 | 
				
			||||||
func TeamsAction(ctx *context.Context) {
 | 
					func TeamsAction(ctx *context.Context) {
 | 
				
			||||||
	uid := ctx.FormInt64("uid")
 | 
					 | 
				
			||||||
	if uid == 0 {
 | 
					 | 
				
			||||||
		ctx.Redirect(ctx.Org.OrgLink + "/teams")
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	page := ctx.FormString("page")
 | 
						page := ctx.FormString("page")
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	switch ctx.Params(":action") {
 | 
						switch ctx.Params(":action") {
 | 
				
			||||||
@@ -77,7 +75,7 @@ func TeamsAction(ctx *context.Context) {
 | 
				
			|||||||
	case "leave":
 | 
						case "leave":
 | 
				
			||||||
		err = models.RemoveTeamMember(ctx.Org.Team, ctx.Doer.ID)
 | 
							err = models.RemoveTeamMember(ctx.Org.Team, ctx.Doer.ID)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if organization.IsErrLastOrgOwner(err) {
 | 
								if org_model.IsErrLastOrgOwner(err) {
 | 
				
			||||||
				ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
									ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				log.Error("Action(%s): %v", ctx.Params(":action"), err)
 | 
									log.Error("Action(%s): %v", ctx.Params(":action"), err)
 | 
				
			||||||
@@ -98,9 +96,16 @@ func TeamsAction(ctx *context.Context) {
 | 
				
			|||||||
			ctx.Error(http.StatusNotFound)
 | 
								ctx.Error(http.StatusNotFound)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							uid := ctx.FormInt64("uid")
 | 
				
			||||||
 | 
							if uid == 0 {
 | 
				
			||||||
 | 
								ctx.Redirect(ctx.Org.OrgLink + "/teams")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		err = models.RemoveTeamMember(ctx.Org.Team, uid)
 | 
							err = models.RemoveTeamMember(ctx.Org.Team, uid)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if organization.IsErrLastOrgOwner(err) {
 | 
								if org_model.IsErrLastOrgOwner(err) {
 | 
				
			||||||
				ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
									ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				log.Error("Action(%s): %v", ctx.Params(":action"), err)
 | 
									log.Error("Action(%s): %v", ctx.Params(":action"), err)
 | 
				
			||||||
@@ -126,10 +131,27 @@ func TeamsAction(ctx *context.Context) {
 | 
				
			|||||||
		u, err = user_model.GetUserByName(ctx, uname)
 | 
							u, err = user_model.GetUserByName(ctx, uname)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if user_model.IsErrUserNotExist(err) {
 | 
								if user_model.IsErrUserNotExist(err) {
 | 
				
			||||||
 | 
									if setting.MailService != nil && user_model.ValidateEmail(uname) == nil {
 | 
				
			||||||
 | 
										invite, err := org_model.CreateTeamInvite(ctx, ctx.Doer, ctx.Org.Team, uname)
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											if org_model.IsErrTeamInviteAlreadyExist(err) {
 | 
				
			||||||
 | 
												ctx.Flash.Error(ctx.Tr("form.duplicate_invite_to_team"))
 | 
				
			||||||
 | 
											} else if org_model.IsErrUserEmailAlreadyAdded(err) {
 | 
				
			||||||
 | 
												ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
 | 
				
			||||||
 | 
											} else {
 | 
				
			||||||
 | 
												ctx.ServerError("CreateTeamInvite", err)
 | 
				
			||||||
 | 
												return
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										} else if err := mailer.MailTeamInvite(ctx, ctx.Doer, ctx.Org.Team, invite); err != nil {
 | 
				
			||||||
 | 
											ctx.ServerError("MailTeamInvite", err)
 | 
				
			||||||
 | 
											return
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
					ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
 | 
										ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
				ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName))
 | 
									ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName))
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.ServerError(" GetUserByName", err)
 | 
									ctx.ServerError("GetUserByName", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -146,11 +168,30 @@ func TeamsAction(ctx *context.Context) {
 | 
				
			|||||||
			err = models.AddTeamMember(ctx.Org.Team, u.ID)
 | 
								err = models.AddTeamMember(ctx.Org.Team, u.ID)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							page = "team"
 | 
				
			||||||
 | 
						case "remove_invite":
 | 
				
			||||||
 | 
							if !ctx.Org.IsOwner {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusNotFound)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							iid := ctx.FormInt64("iid")
 | 
				
			||||||
 | 
							if iid == 0 {
 | 
				
			||||||
 | 
								ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName))
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := org_model.RemoveInviteByID(ctx, iid, ctx.Org.Team.ID); err != nil {
 | 
				
			||||||
 | 
								log.Error("Action(%s): %v", ctx.Params(":action"), err)
 | 
				
			||||||
 | 
								ctx.ServerError("RemoveInviteByID", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		page = "team"
 | 
							page = "team"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if organization.IsErrLastOrgOwner(err) {
 | 
							if org_model.IsErrLastOrgOwner(err) {
 | 
				
			||||||
			ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
								ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			log.Error("Action(%s): %v", ctx.Params(":action"), err)
 | 
								log.Error("Action(%s): %v", ctx.Params(":action"), err)
 | 
				
			||||||
@@ -224,7 +265,7 @@ func NewTeam(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["Title"] = ctx.Org.Organization.FullName
 | 
						ctx.Data["Title"] = ctx.Org.Organization.FullName
 | 
				
			||||||
	ctx.Data["PageIsOrgTeams"] = true
 | 
						ctx.Data["PageIsOrgTeams"] = true
 | 
				
			||||||
	ctx.Data["PageIsOrgTeamsNew"] = true
 | 
						ctx.Data["PageIsOrgTeamsNew"] = true
 | 
				
			||||||
	ctx.Data["Team"] = &organization.Team{}
 | 
						ctx.Data["Team"] = &org_model.Team{}
 | 
				
			||||||
	ctx.Data["Units"] = unit_model.Units
 | 
						ctx.Data["Units"] = unit_model.Units
 | 
				
			||||||
	ctx.HTML(http.StatusOK, tplTeamNew)
 | 
						ctx.HTML(http.StatusOK, tplTeamNew)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -255,7 +296,7 @@ func NewTeamPost(ctx *context.Context) {
 | 
				
			|||||||
		p = unit_model.MinUnitAccessMode(unitPerms)
 | 
							p = unit_model.MinUnitAccessMode(unitPerms)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t := &organization.Team{
 | 
						t := &org_model.Team{
 | 
				
			||||||
		OrgID:                   ctx.Org.Organization.ID,
 | 
							OrgID:                   ctx.Org.Organization.ID,
 | 
				
			||||||
		Name:                    form.TeamName,
 | 
							Name:                    form.TeamName,
 | 
				
			||||||
		Description:             form.Description,
 | 
							Description:             form.Description,
 | 
				
			||||||
@@ -265,9 +306,9 @@ func NewTeamPost(ctx *context.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if t.AccessMode < perm.AccessModeAdmin {
 | 
						if t.AccessMode < perm.AccessModeAdmin {
 | 
				
			||||||
		units := make([]*organization.TeamUnit, 0, len(unitPerms))
 | 
							units := make([]*org_model.TeamUnit, 0, len(unitPerms))
 | 
				
			||||||
		for tp, perm := range unitPerms {
 | 
							for tp, perm := range unitPerms {
 | 
				
			||||||
			units = append(units, &organization.TeamUnit{
 | 
								units = append(units, &org_model.TeamUnit{
 | 
				
			||||||
				OrgID:      ctx.Org.Organization.ID,
 | 
									OrgID:      ctx.Org.Organization.ID,
 | 
				
			||||||
				Type:       tp,
 | 
									Type:       tp,
 | 
				
			||||||
				AccessMode: perm,
 | 
									AccessMode: perm,
 | 
				
			||||||
@@ -295,7 +336,7 @@ func NewTeamPost(ctx *context.Context) {
 | 
				
			|||||||
	if err := models.NewTeam(t); err != nil {
 | 
						if err := models.NewTeam(t); err != nil {
 | 
				
			||||||
		ctx.Data["Err_TeamName"] = true
 | 
							ctx.Data["Err_TeamName"] = true
 | 
				
			||||||
		switch {
 | 
							switch {
 | 
				
			||||||
		case organization.IsErrTeamAlreadyExist(err):
 | 
							case org_model.IsErrTeamAlreadyExist(err):
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
 | 
				
			||||||
		default:
 | 
							default:
 | 
				
			||||||
			ctx.ServerError("NewTeam", err)
 | 
								ctx.ServerError("NewTeam", err)
 | 
				
			||||||
@@ -316,6 +357,15 @@ func TeamMembers(ctx *context.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	ctx.Data["Units"] = unit_model.Units
 | 
						ctx.Data["Units"] = unit_model.Units
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						invites, err := org_model.GetInvitesByTeamID(ctx, ctx.Org.Team.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("GetInvitesByTeamID", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx.Data["Invites"] = invites
 | 
				
			||||||
 | 
						ctx.Data["IsEmailInviteEnabled"] = setting.MailService != nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.HTML(http.StatusOK, tplTeamMembers)
 | 
						ctx.HTML(http.StatusOK, tplTeamMembers)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -339,7 +389,7 @@ func SearchTeam(ctx *context.Context) {
 | 
				
			|||||||
		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 | 
							PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	opts := &organization.SearchTeamOptions{
 | 
						opts := &org_model.SearchTeamOptions{
 | 
				
			||||||
		// UserID is not set because the router already requires the doer to be an org admin. Thus, we don't need to restrict to teams that the user belongs in
 | 
							// UserID is not set because the router already requires the doer to be an org admin. Thus, we don't need to restrict to teams that the user belongs in
 | 
				
			||||||
		Keyword:     ctx.FormTrim("q"),
 | 
							Keyword:     ctx.FormTrim("q"),
 | 
				
			||||||
		OrgID:       ctx.Org.Organization.ID,
 | 
							OrgID:       ctx.Org.Organization.ID,
 | 
				
			||||||
@@ -347,7 +397,7 @@ func SearchTeam(ctx *context.Context) {
 | 
				
			|||||||
		ListOptions: listOptions,
 | 
							ListOptions: listOptions,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	teams, maxResults, err := organization.SearchTeam(opts)
 | 
						teams, maxResults, err := org_model.SearchTeam(opts)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Error("SearchTeam failed: %v", err)
 | 
							log.Error("SearchTeam failed: %v", err)
 | 
				
			||||||
		ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 | 
							ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 | 
				
			||||||
@@ -424,16 +474,16 @@ func EditTeamPost(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	t.Description = form.Description
 | 
						t.Description = form.Description
 | 
				
			||||||
	if t.AccessMode < perm.AccessModeAdmin {
 | 
						if t.AccessMode < perm.AccessModeAdmin {
 | 
				
			||||||
		units := make([]organization.TeamUnit, 0, len(unitPerms))
 | 
							units := make([]org_model.TeamUnit, 0, len(unitPerms))
 | 
				
			||||||
		for tp, perm := range unitPerms {
 | 
							for tp, perm := range unitPerms {
 | 
				
			||||||
			units = append(units, organization.TeamUnit{
 | 
								units = append(units, org_model.TeamUnit{
 | 
				
			||||||
				OrgID:      t.OrgID,
 | 
									OrgID:      t.OrgID,
 | 
				
			||||||
				TeamID:     t.ID,
 | 
									TeamID:     t.ID,
 | 
				
			||||||
				Type:       tp,
 | 
									Type:       tp,
 | 
				
			||||||
				AccessMode: perm,
 | 
									AccessMode: perm,
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if err := organization.UpdateTeamUnits(t, units); err != nil {
 | 
							if err := org_model.UpdateTeamUnits(t, units); err != nil {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "UpdateTeamUnits", err.Error())
 | 
								ctx.Error(http.StatusInternalServerError, "UpdateTeamUnits", err.Error())
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -452,7 +502,7 @@ func EditTeamPost(ctx *context.Context) {
 | 
				
			|||||||
	if err := models.UpdateTeam(t, isAuthChanged, isIncludeAllChanged); err != nil {
 | 
						if err := models.UpdateTeam(t, isAuthChanged, isIncludeAllChanged); err != nil {
 | 
				
			||||||
		ctx.Data["Err_TeamName"] = true
 | 
							ctx.Data["Err_TeamName"] = true
 | 
				
			||||||
		switch {
 | 
							switch {
 | 
				
			||||||
		case organization.IsErrTeamAlreadyExist(err):
 | 
							case org_model.IsErrTeamAlreadyExist(err):
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
 | 
				
			||||||
		default:
 | 
							default:
 | 
				
			||||||
			ctx.ServerError("UpdateTeam", err)
 | 
								ctx.ServerError("UpdateTeam", err)
 | 
				
			||||||
@@ -474,3 +524,72 @@ func DeleteTeam(ctx *context.Context) {
 | 
				
			|||||||
		"redirect": ctx.Org.OrgLink + "/teams",
 | 
							"redirect": ctx.Org.OrgLink + "/teams",
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TeamInvite renders the team invite page
 | 
				
			||||||
 | 
					func TeamInvite(ctx *context.Context) {
 | 
				
			||||||
 | 
						invite, org, team, inviter, err := getTeamInviteFromContext(ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if org_model.IsErrTeamInviteNotFound(err) {
 | 
				
			||||||
 | 
								ctx.NotFound("ErrTeamInviteNotFound", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.ServerError("getTeamInviteFromContext", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Data["Title"] = ctx.Tr("org.teams.invite_team_member", team.Name)
 | 
				
			||||||
 | 
						ctx.Data["Invite"] = invite
 | 
				
			||||||
 | 
						ctx.Data["Organization"] = org
 | 
				
			||||||
 | 
						ctx.Data["Team"] = team
 | 
				
			||||||
 | 
						ctx.Data["Inviter"] = inviter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.HTML(http.StatusOK, tplTeamInvite)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TeamInvitePost handles the team invitation
 | 
				
			||||||
 | 
					func TeamInvitePost(ctx *context.Context) {
 | 
				
			||||||
 | 
						invite, org, team, _, err := getTeamInviteFromContext(ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if org_model.IsErrTeamInviteNotFound(err) {
 | 
				
			||||||
 | 
								ctx.NotFound("ErrTeamInviteNotFound", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.ServerError("getTeamInviteFromContext", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := models.AddTeamMember(team, ctx.Doer.ID); err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("AddTeamMember", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := org_model.RemoveInviteByID(ctx, invite.ID, team.ID); err != nil {
 | 
				
			||||||
 | 
							log.Error("RemoveInviteByID: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Redirect(org.OrganisationLink() + "/teams/" + url.PathEscape(team.LowerName))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getTeamInviteFromContext(ctx *context.Context) (*org_model.TeamInvite, *org_model.Organization, *org_model.Team, *user_model.User, error) {
 | 
				
			||||||
 | 
						invite, err := org_model.GetInviteByToken(ctx, ctx.Params("token"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, nil, nil, nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						inviter, err := user_model.GetUserByIDCtx(ctx, invite.InviterID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, nil, nil, nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						team, err := org_model.GetTeamByID(ctx, invite.TeamID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, nil, nil, nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						org, err := user_model.GetUserByIDCtx(ctx, team.OrgID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, nil, nil, nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return invite, org_model.OrgFromUser(org), team, inviter, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -651,6 +651,11 @@ func RegisterRoutes(m *web.Route) {
 | 
				
			|||||||
			m.Post("/create", bindIgnErr(forms.CreateOrgForm{}), org.CreatePost)
 | 
								m.Post("/create", bindIgnErr(forms.CreateOrgForm{}), org.CreatePost)
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							m.Group("/invite/{token}", func() {
 | 
				
			||||||
 | 
								m.Get("", org.TeamInvite)
 | 
				
			||||||
 | 
								m.Post("", org.TeamInvitePost)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		m.Group("/{org}", func() {
 | 
							m.Group("/{org}", func() {
 | 
				
			||||||
			m.Get("/dashboard", user.Dashboard)
 | 
								m.Get("/dashboard", user.Dashboard)
 | 
				
			||||||
			m.Get("/dashboard/{team}", user.Dashboard)
 | 
								m.Get("/dashboard/{team}", user.Dashboard)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,7 @@ const (
 | 
				
			|||||||
	tplNewReleaseMail base.TplName = "release"
 | 
						tplNewReleaseMail base.TplName = "release"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// MailNewRelease send new release notify to all all repo watchers.
 | 
					// MailNewRelease send new release notify to all repo watchers.
 | 
				
			||||||
func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
 | 
					func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
 | 
				
			||||||
	if setting.MailService == nil {
 | 
						if setting.MailService == nil {
 | 
				
			||||||
		// No mail service configured
 | 
							// No mail service configured
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										62
									
								
								services/mailer/mail_team_invite.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								services/mailer/mail_team_invite.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package mailer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						org_model "code.gitea.io/gitea/models/organization"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/templates"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/translation"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						tplTeamInviteMail base.TplName = "team_invite"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MailTeamInvite sends team invites
 | 
				
			||||||
 | 
					func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_model.Team, invite *org_model.TeamInvite) error {
 | 
				
			||||||
 | 
						if setting.MailService == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						org, err := user_model.GetUserByIDCtx(ctx, team.OrgID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						locale := translation.NewLocale(inviter.Language)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						subject := locale.Tr("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName())
 | 
				
			||||||
 | 
						mailMeta := map[string]interface{}{
 | 
				
			||||||
 | 
							"Inviter":      inviter,
 | 
				
			||||||
 | 
							"Organization": org,
 | 
				
			||||||
 | 
							"Team":         team,
 | 
				
			||||||
 | 
							"Invite":       invite,
 | 
				
			||||||
 | 
							"Subject":      subject,
 | 
				
			||||||
 | 
							// helper
 | 
				
			||||||
 | 
							"locale":    locale,
 | 
				
			||||||
 | 
							"Str2html":  templates.Str2html,
 | 
				
			||||||
 | 
							"DotEscape": templates.DotEscape,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var mailBody bytes.Buffer
 | 
				
			||||||
 | 
						if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplTeamInviteMail), mailMeta); err != nil {
 | 
				
			||||||
 | 
							log.Error("ExecuteTemplate [%s]: %v", string(tplTeamInviteMail)+"/body", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						msg := NewMessage([]string{invite.Email}, subject, mailBody.String())
 | 
				
			||||||
 | 
						msg.Info = subject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						SendAsync(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								templates/mail/team_invite.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								templates/mail/team_invite.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
						<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 | 
				
			||||||
 | 
						<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no"/>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					{{$invite_url := printf "%sorg/invite/%s" AppUrl (QueryEscape .Invite.Token)}}
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
						<p>{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName) | Str2html}}</p>
 | 
				
			||||||
 | 
						<p>{{.locale.Tr "mail.team_invite.text_2"}}</p><p><a href="{{$invite_url}}">{{$invite_url}}</a></p>
 | 
				
			||||||
 | 
						<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
 | 
				
			||||||
 | 
						<p>{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										23
									
								
								templates/org/team/invite.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								templates/org/team/invite.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					{{template "base/head" .}}
 | 
				
			||||||
 | 
					<div class="page-content organization invite">
 | 
				
			||||||
 | 
						<div class="ui container">
 | 
				
			||||||
 | 
							{{template "base/alert" .}}
 | 
				
			||||||
 | 
							<div class="ui centered card">
 | 
				
			||||||
 | 
								<div class="image">
 | 
				
			||||||
 | 
									{{avatar .Organization 140}}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="content">
 | 
				
			||||||
 | 
									<div class="header">{{.locale.Tr "org.teams.invite.title" .Team.Name .Organization.Name | Str2html}}</div>
 | 
				
			||||||
 | 
									<div class="meta">{{.locale.Tr "org.teams.invite.by" .Inviter.Name}}</div>
 | 
				
			||||||
 | 
									<div class="description">{{.locale.Tr "org.teams.invite.description"}}</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="extra content">
 | 
				
			||||||
 | 
									<form class="ui form" action="" method="post">
 | 
				
			||||||
 | 
										{{.CsrfTokenHtml}}
 | 
				
			||||||
 | 
										<button class="fluid ui green button">{{.locale.Tr "org.teams.join"}}</button>
 | 
				
			||||||
 | 
									</form>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{{template "base/footer" .}}
 | 
				
			||||||
@@ -13,7 +13,7 @@
 | 
				
			|||||||
							{{.CsrfTokenHtml}}
 | 
												{{.CsrfTokenHtml}}
 | 
				
			||||||
							<input type="hidden" name="uid" value="{{.SignedUser.ID}}">
 | 
												<input type="hidden" name="uid" value="{{.SignedUser.ID}}">
 | 
				
			||||||
							<div class="inline field ui left">
 | 
												<div class="inline field ui left">
 | 
				
			||||||
								<div id="search-user-box" class="ui search">
 | 
													<div id="search-user-box" class="ui search"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{.locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}>
 | 
				
			||||||
									<div class="ui input">
 | 
														<div class="ui input">
 | 
				
			||||||
										<input class="prompt" name="uname" placeholder="{{.locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
 | 
															<input class="prompt" name="uname" placeholder="{{.locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
 | 
				
			||||||
									</div>
 | 
														</div>
 | 
				
			||||||
@@ -45,6 +45,21 @@
 | 
				
			|||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					{{end}}
 | 
										{{end}}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
 | 
									{{if and .Invites $.IsOrganizationOwner}}
 | 
				
			||||||
 | 
									<h4 class="ui top attached header">{{$.locale.Tr "org.teams.invite_team_member.list"}}</h4>
 | 
				
			||||||
 | 
									<div class="ui bottom attached table segment members">
 | 
				
			||||||
 | 
										{{range .Invites}}
 | 
				
			||||||
 | 
											<div class="item">
 | 
				
			||||||
 | 
												<form action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/remove_invite" method="post">
 | 
				
			||||||
 | 
													{{$.CsrfTokenHtml}}
 | 
				
			||||||
 | 
													<input type="hidden" name="iid" value="{{.ID}}" />
 | 
				
			||||||
 | 
													<button class="ui red button right">{{$.locale.Tr "org.members.remove"}}</button>
 | 
				
			||||||
 | 
												</form>
 | 
				
			||||||
 | 
												{{.Email}}
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										{{end}}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										72
									
								
								tests/integration/org_team_invite_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								tests/integration/org_team_invite_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/organization"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/test"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/tests"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestOrgTeamEmailInvite(t *testing.T) {
 | 
				
			||||||
 | 
						if setting.MailService == nil {
 | 
				
			||||||
 | 
							t.Skip()
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
 | 
				
			||||||
 | 
						team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
 | 
				
			||||||
 | 
						user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.False(t, isMember)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session := loginUser(t, "user1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						url := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
 | 
				
			||||||
 | 
						csrf := GetCSRF(t, session, url)
 | 
				
			||||||
 | 
						req := NewRequestWithValues(t, "POST", url+"/action/add", map[string]string{
 | 
				
			||||||
 | 
							"_csrf": csrf,
 | 
				
			||||||
 | 
							"uid":   "1",
 | 
				
			||||||
 | 
							"uname": user.Email,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						resp := session.MakeRequest(t, req, http.StatusSeeOther)
 | 
				
			||||||
 | 
						req = NewRequest(t, "GET", test.RedirectURL(resp))
 | 
				
			||||||
 | 
						session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// get the invite token
 | 
				
			||||||
 | 
						invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, invites, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session = loginUser(t, user.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// join the team
 | 
				
			||||||
 | 
						url = fmt.Sprintf("/org/invite/%s", invites[0].Token)
 | 
				
			||||||
 | 
						csrf = GetCSRF(t, session, url)
 | 
				
			||||||
 | 
						req = NewRequestWithValues(t, "POST", url, map[string]string{
 | 
				
			||||||
 | 
							"_csrf": csrf,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusSeeOther)
 | 
				
			||||||
 | 
						req = NewRequest(t, "GET", test.RedirectURL(resp))
 | 
				
			||||||
 | 
						session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.True(t, isMember)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,15 +3,20 @@ import {htmlEscape} from 'escape-goat';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const {appSubUrl} = window.config;
 | 
					const {appSubUrl} = window.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const looksLikeEmailAddressCheck = /^\S+@\S+$/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function initCompSearchUserBox() {
 | 
					export function initCompSearchUserBox() {
 | 
				
			||||||
  const $searchUserBox = $('#search-user-box');
 | 
					  const $searchUserBox = $('#search-user-box');
 | 
				
			||||||
 | 
					  const allowEmailInput = $searchUserBox.attr('data-allow-email') === 'true';
 | 
				
			||||||
 | 
					  const allowEmailDescription = $searchUserBox.attr('data-allow-email-description');
 | 
				
			||||||
  $searchUserBox.search({
 | 
					  $searchUserBox.search({
 | 
				
			||||||
    minCharacters: 2,
 | 
					    minCharacters: 2,
 | 
				
			||||||
    apiSettings: {
 | 
					    apiSettings: {
 | 
				
			||||||
      url: `${appSubUrl}/user/search?q={query}`,
 | 
					      url: `${appSubUrl}/user/search?q={query}`,
 | 
				
			||||||
      onResponse(response) {
 | 
					      onResponse(response) {
 | 
				
			||||||
        const items = [];
 | 
					        const items = [];
 | 
				
			||||||
        const searchQueryUppercase = $searchUserBox.find('input').val().toUpperCase();
 | 
					        const searchQuery = $searchUserBox.find('input').val();
 | 
				
			||||||
 | 
					        const searchQueryUppercase = searchQuery.toUpperCase();
 | 
				
			||||||
        $.each(response.data, (_i, item) => {
 | 
					        $.each(response.data, (_i, item) => {
 | 
				
			||||||
          let title = item.login;
 | 
					          let title = item.login;
 | 
				
			||||||
          if (item.full_name && item.full_name.length > 0) {
 | 
					          if (item.full_name && item.full_name.length > 0) {
 | 
				
			||||||
@@ -28,6 +33,14 @@ export function initCompSearchUserBox() {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (allowEmailInput && items.length === 0 && looksLikeEmailAddressCheck.test(searchQuery)) {
 | 
				
			||||||
 | 
					          const resultItem = {
 | 
				
			||||||
 | 
					            title: searchQuery,
 | 
				
			||||||
 | 
					            description: allowEmailDescription
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					          items.push(resultItem);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return {results: items};
 | 
					        return {results: items};
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -119,6 +119,11 @@
 | 
				
			|||||||
        margin-top: -3px;
 | 
					        margin-top: -3px;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .ui.avatar {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      height: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.members {
 | 
					  &.members {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user