Skip to content

Commit 8c21404

Browse files
committed
Merge remote-tracking branch 'upstream/main'
* upstream/main: [skip ci] Updated translations via Crowdin Check for valid user token in integration tests (go-gitea#21520) Ignore error when retrieving changed PR review files (go-gitea#21487) move invite by mail to services package (go-gitea#21513) Enable Monaco automaticLayout (go-gitea#21515) Update macOS install command (go-gitea#21507) [skip ci] Updated translations via Crowdin Suppress `ExternalLoginUserNotExist` error (go-gitea#21504) Revert increased width on pull pages (go-gitea#21470) Add team member invite by email (go-gitea#20307) Disable the 'Add File' button when not able to edit repo (go-gitea#21503) Remove vitest globals (go-gitea#21505) Fix branch dropdown shifting on page load (go-gitea#21428)
2 parents ffaded3 + da3b657 commit 8c21404

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+738
-93
lines changed

.eslintrc.yaml

-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ overrides:
3535
rules:
3636
import/no-unresolved: [0]
3737
import/no-extraneous-dependencies: [0]
38-
- files: ["*.test.js"]
39-
env:
40-
jest: true
4138
- files: ["*.config.js"]
4239
rules:
4340
import/no-unused-modules: [0]

docs/content/doc/installation/from-package.zh-cn.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ choco install gitea
7171
macOS 平台下当前我们仅支持通过 `brew` 来安装。如果你没有安装 [Homebrew](http://brew.sh/),你也可以查看 [从二进制安装]({{< relref "from-binary.zh-cn.md" >}})。在你安装了 `brew` 之后, 你可以执行以下命令:
7272

7373
```
74-
brew tap go-gitea/gitea
74+
brew tap gitea/tap https://gitea.com/gitea/homebrew-gitea
7575
brew install gitea
7676
```
7777

models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,8 @@ var migrations = []Migration{
417417
NewMigration("Conan and generic packages do not need to be semantically versioned", fixPackageSemverField),
418418
// v227 -> v228
419419
NewMigration("Create key/value table for system settings", createSystemSettingsTable),
420+
// v228 -> v229
421+
NewMigration("Add TeamInvite table", addTeamInviteTable),
420422
}
421423

422424
// GetCurrentDBVersion returns the current db version

models/migrations/v228.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"code.gitea.io/gitea/modules/timeutil"
9+
10+
"xorm.io/xorm"
11+
)
12+
13+
func addTeamInviteTable(x *xorm.Engine) error {
14+
type TeamInvite struct {
15+
ID int64 `xorm:"pk autoincr"`
16+
Token string `xorm:"UNIQUE(token) INDEX NOT NULL DEFAULT ''"`
17+
InviterID int64 `xorm:"NOT NULL DEFAULT 0"`
18+
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0"`
19+
TeamID int64 `xorm:"UNIQUE(team_mail) INDEX NOT NULL DEFAULT 0"`
20+
Email string `xorm:"UNIQUE(team_mail) NOT NULL DEFAULT ''"`
21+
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
22+
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
23+
}
24+
25+
return x.Sync2(new(TeamInvite))
26+
}

models/org_team.go

+6-16
Original file line numberDiff line numberDiff line change
@@ -431,25 +431,15 @@ func DeleteTeam(t *organization.Team) error {
431431
}
432432
}
433433

434-
// Delete team-user.
435-
if _, err := sess.
436-
Where("org_id=?", t.OrgID).
437-
Where("team_id=?", t.ID).
438-
Delete(new(organization.TeamUser)); err != nil {
439-
return err
440-
}
441-
442-
// Delete team-unit.
443-
if _, err := sess.
444-
Where("team_id=?", t.ID).
445-
Delete(new(organization.TeamUnit)); err != nil {
434+
if err := db.DeleteBeans(ctx,
435+
&organization.Team{ID: t.ID},
436+
&organization.TeamUser{OrgID: t.OrgID, TeamID: t.ID},
437+
&organization.TeamUnit{TeamID: t.ID},
438+
&organization.TeamInvite{TeamID: t.ID},
439+
); err != nil {
446440
return err
447441
}
448442

449-
// Delete team.
450-
if _, err := sess.ID(t.ID).Delete(new(organization.Team)); err != nil {
451-
return err
452-
}
453443
// Update organization number of teams.
454444
if _, err := sess.Exec("UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil {
455445
return err

models/organization/org.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,9 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
370370
&OrgUser{OrgID: org.ID},
371371
&TeamUser{OrgID: org.ID},
372372
&TeamUnit{OrgID: org.ID},
373+
&TeamInvite{OrgID: org.ID},
373374
); err != nil {
374-
return fmt.Errorf("deleteBeans: %v", err)
375+
return fmt.Errorf("DeleteBeans: %v", err)
375376
}
376377

377378
if _, err := db.GetEngine(ctx).ID(org.ID).Delete(new(user_model.User)); err != nil {

models/organization/team.go

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func init() {
9494
db.RegisterModel(new(TeamUser))
9595
db.RegisterModel(new(TeamRepo))
9696
db.RegisterModel(new(TeamUnit))
97+
db.RegisterModel(new(TeamInvite))
9798
}
9899

99100
// SearchTeamOptions holds the search options

models/organization/team_invite.go

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package organization
6+
7+
import (
8+
"context"
9+
"fmt"
10+
11+
"code.gitea.io/gitea/models/db"
12+
user_model "code.gitea.io/gitea/models/user"
13+
"code.gitea.io/gitea/modules/timeutil"
14+
"code.gitea.io/gitea/modules/util"
15+
16+
"xorm.io/builder"
17+
)
18+
19+
type ErrTeamInviteAlreadyExist struct {
20+
TeamID int64
21+
Email string
22+
}
23+
24+
func IsErrTeamInviteAlreadyExist(err error) bool {
25+
_, ok := err.(ErrTeamInviteAlreadyExist)
26+
return ok
27+
}
28+
29+
func (err ErrTeamInviteAlreadyExist) Error() string {
30+
return fmt.Sprintf("team invite already exists [team_id: %d, email: %s]", err.TeamID, err.Email)
31+
}
32+
33+
func (err ErrTeamInviteAlreadyExist) Unwrap() error {
34+
return util.ErrAlreadyExist
35+
}
36+
37+
type ErrTeamInviteNotFound struct {
38+
Token string
39+
}
40+
41+
func IsErrTeamInviteNotFound(err error) bool {
42+
_, ok := err.(ErrTeamInviteNotFound)
43+
return ok
44+
}
45+
46+
func (err ErrTeamInviteNotFound) Error() string {
47+
return fmt.Sprintf("team invite was not found [token: %s]", err.Token)
48+
}
49+
50+
func (err ErrTeamInviteNotFound) Unwrap() error {
51+
return util.ErrNotExist
52+
}
53+
54+
// ErrUserEmailAlreadyAdded represents a "user by email already added to team" error.
55+
type ErrUserEmailAlreadyAdded struct {
56+
Email string
57+
}
58+
59+
// IsErrUserEmailAlreadyAdded checks if an error is a ErrUserEmailAlreadyAdded.
60+
func IsErrUserEmailAlreadyAdded(err error) bool {
61+
_, ok := err.(ErrUserEmailAlreadyAdded)
62+
return ok
63+
}
64+
65+
func (err ErrUserEmailAlreadyAdded) Error() string {
66+
return fmt.Sprintf("user with email already added [email: %s]", err.Email)
67+
}
68+
69+
func (err ErrUserEmailAlreadyAdded) Unwrap() error {
70+
return util.ErrAlreadyExist
71+
}
72+
73+
// TeamInvite represents an invite to a team
74+
type TeamInvite struct {
75+
ID int64 `xorm:"pk autoincr"`
76+
Token string `xorm:"UNIQUE(token) INDEX NOT NULL DEFAULT ''"`
77+
InviterID int64 `xorm:"NOT NULL DEFAULT 0"`
78+
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0"`
79+
TeamID int64 `xorm:"UNIQUE(team_mail) INDEX NOT NULL DEFAULT 0"`
80+
Email string `xorm:"UNIQUE(team_mail) NOT NULL DEFAULT ''"`
81+
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
82+
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
83+
}
84+
85+
func CreateTeamInvite(ctx context.Context, doer *user_model.User, team *Team, email string) (*TeamInvite, error) {
86+
has, err := db.GetEngine(ctx).Exist(&TeamInvite{
87+
TeamID: team.ID,
88+
Email: email,
89+
})
90+
if err != nil {
91+
return nil, err
92+
}
93+
if has {
94+
return nil, ErrTeamInviteAlreadyExist{
95+
TeamID: team.ID,
96+
Email: email,
97+
}
98+
}
99+
100+
// check if the user is already a team member by email
101+
exist, err := db.GetEngine(ctx).
102+
Where(builder.Eq{
103+
"team_user.org_id": team.OrgID,
104+
"team_user.team_id": team.ID,
105+
"`user`.email": email,
106+
}).
107+
Join("INNER", "`user`", "`user`.id = team_user.uid").
108+
Table("team_user").
109+
Exist()
110+
if err != nil {
111+
return nil, err
112+
}
113+
114+
if exist {
115+
return nil, ErrUserEmailAlreadyAdded{
116+
Email: email,
117+
}
118+
}
119+
120+
token, err := util.CryptoRandomString(25)
121+
if err != nil {
122+
return nil, err
123+
}
124+
125+
invite := &TeamInvite{
126+
Token: token,
127+
InviterID: doer.ID,
128+
OrgID: team.OrgID,
129+
TeamID: team.ID,
130+
Email: email,
131+
}
132+
133+
return invite, db.Insert(ctx, invite)
134+
}
135+
136+
func RemoveInviteByID(ctx context.Context, inviteID, teamID int64) error {
137+
_, err := db.DeleteByBean(ctx, &TeamInvite{
138+
ID: inviteID,
139+
TeamID: teamID,
140+
})
141+
return err
142+
}
143+
144+
func GetInvitesByTeamID(ctx context.Context, teamID int64) ([]*TeamInvite, error) {
145+
invites := make([]*TeamInvite, 0, 10)
146+
return invites, db.GetEngine(ctx).
147+
Where("team_id=?", teamID).
148+
Find(&invites)
149+
}
150+
151+
func GetInviteByToken(ctx context.Context, token string) (*TeamInvite, error) {
152+
invite := &TeamInvite{}
153+
154+
has, err := db.GetEngine(ctx).Where("token=?", token).Get(invite)
155+
if err != nil {
156+
return nil, err
157+
}
158+
if !has {
159+
return nil, ErrTeamInviteNotFound{Token: token}
160+
}
161+
return invite, nil
162+
}
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package organization_test
6+
7+
import (
8+
"testing"
9+
10+
"code.gitea.io/gitea/models/db"
11+
"code.gitea.io/gitea/models/organization"
12+
"code.gitea.io/gitea/models/unittest"
13+
user_model "code.gitea.io/gitea/models/user"
14+
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
func TestTeamInvite(t *testing.T) {
19+
assert.NoError(t, unittest.PrepareTestDatabase())
20+
21+
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
22+
23+
t.Run("MailExistsInTeam", func(t *testing.T) {
24+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
25+
26+
// user 2 already added to team 2, should result in error
27+
_, err := organization.CreateTeamInvite(db.DefaultContext, user2, team, user2.Email)
28+
assert.Error(t, err)
29+
})
30+
31+
t.Run("CreateAndRemove", func(t *testing.T) {
32+
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
33+
34+
invite, err := organization.CreateTeamInvite(db.DefaultContext, user1, team, "[email protected]")
35+
assert.NotNil(t, invite)
36+
assert.NoError(t, err)
37+
38+
// Shouldn't allow duplicate invite
39+
_, err = organization.CreateTeamInvite(db.DefaultContext, user1, team, "[email protected]")
40+
assert.Error(t, err)
41+
42+
// should remove invite
43+
assert.NoError(t, organization.RemoveInviteByID(db.DefaultContext, invite.ID, invite.TeamID))
44+
45+
// invite should not exist
46+
_, err = organization.GetInviteByToken(db.DefaultContext, invite.Token)
47+
assert.Error(t, err)
48+
})
49+
}

options/locale/locale_bg-BG.ini

+1
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ register_success=Успешна регистрация
261261

262262

263263

264+
264265
[modal]
265266
yes=Да
266267
no=Не

options/locale/locale_cs-CZ.ini

+1
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ repo.transfer.body=Chcete-li ji přijmout nebo odmítnout, navštivte %s nebo ji
412412
repo.collaborator.added.subject=%s vás přidal do %s
413413
repo.collaborator.added.text=Byl jste přidán jako spolupracovník repozitáře:
414414

415+
415416
[modal]
416417
yes=Ano
417418
no=Ne

options/locale/locale_de-DE.ini

+1
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ repo.transfer.body=Um es anzunehmen oder abzulehnen, öffne %s, oder ignoriere e
407407
repo.collaborator.added.subject=%s hat dich zu %s hinzugefügt
408408
repo.collaborator.added.text=Du wurdest als Mitarbeiter für folgendes Repository hinzugefügt:
409409

410+
410411
[modal]
411412
yes=Ja
412413
no=Abbrechen

options/locale/locale_el-GR.ini

+1
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ repo.transfer.body=Για να το αποδεχτείτε ή να το απορ
409409
repo.collaborator.added.subject=%s σας πρόσθεσε στο %s
410410
repo.collaborator.added.text=Έχετε προστεθεί ως συνεργάτης του αποθετηρίου:
411411

412+
412413
[modal]
413414
yes=Ναι
414415
no=Όχι

options/locale/locale_en-US.ini

+11
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,11 @@ repo.transfer.body = To accept or reject it visit %s or just ignore it.
412412
repo.collaborator.added.subject = %s added you to %s
413413
repo.collaborator.added.text = You have been added as a collaborator of repository:
414414

415+
team_invite.subject = %[1]s has invited you to join the %[2]s organization
416+
team_invite.text_1 = %[1]s has invited you to join team %[2]s in organization %[3]s.
417+
team_invite.text_2 = Please click the following link to join the team:
418+
team_invite.text_3 = Note: This invitation was intended for %[1]s. If you were not expecting this invitation, you can ignore this email.
419+
415420
[modal]
416421
yes = Yes
417422
no = No
@@ -487,6 +492,7 @@ user_not_exist = The user does not exist.
487492
team_not_exist = The team does not exist.
488493
last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner for an organization.
489494
cannot_add_org_to_team = An organization cannot be added as a team member.
495+
duplicate_invite_to_team = The user was already invited as a team member.
490496

491497
invalid_ssh_key = Can not verify your SSH key: %s
492498
invalid_gpg_key = Can not verify your GPG key: %s
@@ -2402,6 +2408,8 @@ teams.members = Team Members
24022408
teams.update_settings = Update Settings
24032409
teams.delete_team = Delete Team
24042410
teams.add_team_member = Add Team Member
2411+
teams.invite_team_member = Invite to %s
2412+
teams.invite_team_member.list = Pending Invitations
24052413
teams.delete_team_title = Delete Team
24062414
teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue?
24072415
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
24262434
teams.all_repositories_read_permission_desc = This team grants <strong>Read</strong> access to <strong>all repositories</strong>: members can view and clone repositories.
24272435
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.
24282436
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.
2437+
teams.invite.title = You've been invited to join team <strong>%s</strong> in organization <strong>%s</strong>.
2438+
teams.invite.by = Invited by %s
2439+
teams.invite.description = Please click the button below to join the team.
24292440
24302441
[admin]
24312442
dashboard = Dashboard

options/locale/locale_es-ES.ini

+1
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ repo.transfer.body=Para aceptarlo o rechazarlo, visita %s o simplemente ignórel
411411
repo.collaborator.added.subject=%s le añadió en %s
412412
repo.collaborator.added.text=Has sido añadido como colaborador del repositorio:
413413

414+
414415
[modal]
415416
yes=Sí
416417
no=No

0 commit comments

Comments
 (0)