-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add team member invite by email (#20307)
Allows to add (not registered) team members by email. related #5353 Invite by mail: ![grafik](https://user-images.githubusercontent.com/1666336/178154779-adcc547f-c0b7-4a2a-a131-4e41a3d9d3ad.png) Pending invitations: ![grafik](https://user-images.githubusercontent.com/1666336/178154882-9d739bb8-2b04-46c1-a025-c1f4be26af98.png) Email: ![grafik](https://user-images.githubusercontent.com/1666336/178164716-f2f90893-7ba6-4a5e-a3db-42538a660258.png) Join form: ![grafik](https://user-images.githubusercontent.com/1666336/178154840-aaab983a-d922-4414-b01a-9b1a19c5cef7.png) Co-authored-by: Jack Hay <jjphay@gmail.com>
- Loading branch information
Showing
18 changed files
with
615 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.