mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-05-31 11:52:10 +00: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:
parent
7d1aed83f4
commit
c3b2e44392
18 changed files with 615 additions and 43 deletions
|
@ -417,6 +417,8 @@ var migrations = []Migration{
|
|||
NewMigration("Conan and generic packages do not need to be semantically versioned", fixPackageSemverField),
|
||||
// v227 -> v228
|
||||
NewMigration("Create key/value table for system settings", createSystemSettingsTable),
|
||||
// v228 -> v229
|
||||
NewMigration("Add TeamInvite table", addTeamInviteTable),
|
||||
}
|
||||
|
||||
// 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 := sess.
|
||||
Where("org_id=?", t.OrgID).
|
||||
Where("team_id=?", t.ID).
|
||||
Delete(new(organization.TeamUser)); err != nil {
|
||||
if err := db.DeleteBeans(ctx,
|
||||
&organization.Team{ID: t.ID},
|
||||
&organization.TeamUser{OrgID: t.OrgID, TeamID: t.ID},
|
||||
&organization.TeamUnit{TeamID: t.ID},
|
||||
&organization.TeamInvite{TeamID: t.ID},
|
||||
); err != nil {
|
||||
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.
|
||||
if _, err := sess.Exec("UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil {
|
||||
return err
|
||||
|
|
|
@ -370,8 +370,9 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
|
|||
&OrgUser{OrgID: org.ID},
|
||||
&TeamUser{OrgID: org.ID},
|
||||
&TeamUnit{OrgID: org.ID},
|
||||
&TeamInvite{OrgID: org.ID},
|
||||
); 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 {
|
||||
|
|
|
@ -94,6 +94,7 @@ func init() {
|
|||
db.RegisterModel(new(TeamUser))
|
||||
db.RegisterModel(new(TeamRepo))
|
||||
db.RegisterModel(new(TeamUnit))
|
||||
db.RegisterModel(new(TeamInvite))
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue