diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 66d4452ec5072..4dafc8f492186 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -35,9 +35,6 @@ overrides: rules: import/no-unresolved: [0] import/no-extraneous-dependencies: [0] - - files: ["*.test.js"] - env: - jest: true - files: ["*.config.js"] rules: import/no-unused-modules: [0] diff --git a/docs/content/doc/installation/from-package.zh-cn.md b/docs/content/doc/installation/from-package.zh-cn.md index 7faf2a88b1444..dd56ebdaa2d55 100644 --- a/docs/content/doc/installation/from-package.zh-cn.md +++ b/docs/content/doc/installation/from-package.zh-cn.md @@ -71,7 +71,7 @@ choco install gitea macOS 平台下当前我们仅支持通过 `brew` 来安装。如果你没有安装 [Homebrew](http://brew.sh/),你也可以查看 [从二进制安装]({{< relref "from-binary.zh-cn.md" >}})。在你安装了 `brew` 之后, 你可以执行以下命令: ``` -brew tap go-gitea/gitea +brew tap gitea/tap https://gitea.com/gitea/homebrew-gitea brew install gitea ``` diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index afe1445a23096..46ef052829b6d 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -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 diff --git a/models/migrations/v228.go b/models/migrations/v228.go new file mode 100644 index 0000000000000..62c81ef9d8c24 --- /dev/null +++ b/models/migrations/v228.go @@ -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)) +} diff --git a/models/org_team.go b/models/org_team.go index 61ddd2a047a8f..6066e7f5c9b17 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -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 { - return err - } - - // Delete team-unit. - if _, err := sess. - Where("team_id=?", t.ID). - Delete(new(organization.TeamUnit)); 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. - 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 diff --git a/models/organization/org.go b/models/organization/org.go index fbbf6d04fac6e..58b58e6732493 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -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 { diff --git a/models/organization/team.go b/models/organization/team.go index 83e5bd6fe1e14..aa9b24b57f439 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -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 diff --git a/models/organization/team_invite.go b/models/organization/team_invite.go new file mode 100644 index 0000000000000..4504a2e9fef30 --- /dev/null +++ b/models/organization/team_invite.go @@ -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 +} diff --git a/models/organization/team_invite_test.go b/models/organization/team_invite_test.go new file mode 100644 index 0000000000000..e0596ec28da57 --- /dev/null +++ b/models/organization/team_invite_test.go @@ -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) + }) +} diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini index 10759528e5f00..24617e648d1c2 100644 --- a/options/locale/locale_bg-BG.ini +++ b/options/locale/locale_bg-BG.ini @@ -261,6 +261,7 @@ register_success=Успешна регистрация + [modal] yes=Да no=Не diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 654a9480f0ab5..6da1cac65d561 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -412,6 +412,7 @@ repo.transfer.body=Chcete-li ji přijmout nebo odmítnout, navštivte %s nebo ji repo.collaborator.added.subject=%s vás přidal do %s repo.collaborator.added.text=Byl jste přidán jako spolupracovník repozitáře: + [modal] yes=Ano no=Ne diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 86c442716df3a..789774e322f4a 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -407,6 +407,7 @@ repo.transfer.body=Um es anzunehmen oder abzulehnen, öffne %s, oder ignoriere e repo.collaborator.added.subject=%s hat dich zu %s hinzugefügt repo.collaborator.added.text=Du wurdest als Mitarbeiter für folgendes Repository hinzugefügt: + [modal] yes=Ja no=Abbrechen diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index abf504dda0a05..62ef37d4b1487 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -409,6 +409,7 @@ repo.transfer.body=Για να το αποδεχτείτε ή να το απορ repo.collaborator.added.subject=%s σας πρόσθεσε στο %s repo.collaborator.added.text=Έχετε προστεθεί ως συνεργάτης του αποθετηρίου: + [modal] yes=Ναι no=Όχι diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e5da074f64b47..a35c6a668af4a 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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.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] yes = Yes no = No @@ -487,6 +492,7 @@ user_not_exist = The user 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. 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_gpg_key = Can not verify your GPG key: %s @@ -2402,6 +2408,8 @@ teams.members = Team Members teams.update_settings = Update Settings teams.delete_team = Delete Team 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_desc = Deleting a team revokes repository access from its members. Continue? 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 Read access to all repositories: members can view and clone repositories. teams.all_repositories_write_permission_desc = This team grants Write access to all repositories: members can read from and push to repositories. teams.all_repositories_admin_permission_desc = This team grants Admin access to all repositories: members can read from, push to and add collaborators to repositories. +teams.invite.title = You've been invited to join team %s in organization %s. +teams.invite.by = Invited by %s +teams.invite.description = Please click the button below to join the team. [admin] dashboard = Dashboard diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 7588d53f1fb42..31264ea9bdb49 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -411,6 +411,7 @@ repo.transfer.body=Para aceptarlo o rechazarlo, visita %s o simplemente ignórel repo.collaborator.added.subject=%s le añadió en %s repo.collaborator.added.text=Has sido añadido como colaborador del repositorio: + [modal] yes=Sí no=No diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 9e012ff557c93..e86480d0bc303 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -379,6 +379,7 @@ repo.transfer.body=برای تایید یا رد آن %s را ببینید یا repo.collaborator.added.subject=%s شما را به پروژه %s اضافه کرد repo.collaborator.added.text=شما به عنوان مشارکتکننده در این مخزن اضافه شدید: + [modal] yes=بله no=خیر diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index faac82b0cfdab..7f13d1b807fac 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -358,6 +358,7 @@ release.download.targz=Lähdekoodi (TAR.GZ) repo.transfer.to_you=sinä + [modal] yes=Kyllä no=Ei diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 600f43049f02a..eaedeaf9c3d02 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -412,6 +412,7 @@ repo.transfer.body=Pour l'accepter ou le rejeter, visitez %s ou ignorez-le. repo.collaborator.added.subject=%s vous a ajouté à %s repo.collaborator.added.text=Vous avez été ajouté en tant que collaborateur du dépôt : + [modal] yes=Oui no=Non diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index d3d31f30ef20d..c5bd8c5af28e1 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -291,6 +291,7 @@ register_success=Sikeres regisztráció + [modal] yes=Igen no=Nem diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 53d6006c963ec..8e537abf71b52 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -282,6 +282,7 @@ register_success=Pendaftaran berhasil + [modal] yes=Ya no=Tidak diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index c6b747826a804..2e33940b99339 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -339,6 +339,7 @@ repo.transfer.body=Til að samþykkja eða hafna því skaltu fara á %s eða hu repo.collaborator.added.subject=%s bætti þér við í %s repo.collaborator.added.text=Þér hefur verið bætt við sem aðila hugbúnaðarsafns: + [modal] yes=Já no=Nei diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index f002c534d7d20..2a159dda6fc87 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -407,6 +407,7 @@ repo.transfer.body=Per accettare o respingerla visita %s o semplicemente ignorar repo.collaborator.added.subject=%s ti ha aggiunto a %s repo.collaborator.added.text=Sei stato aggiunto come collaboratore del repository: + [modal] yes=Sì no=No diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 3b392365457e5..a4c6349eee116 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -412,6 +412,7 @@ repo.transfer.body=承認または拒否するには %s を開きます。 も repo.collaborator.added.subject=%s が %s にあなたを追加しました repo.collaborator.added.text=あなたは次のリポジトリの共同作業者に追加されました: + [modal] yes=はい no=いいえ diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index f4f682cb7afd2..db2cee952bce1 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -273,6 +273,7 @@ register_success=등록 완료 + [modal] yes=예 no=아니오 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index e06ff0a7ff925..d6fd4fee7573c 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -407,6 +407,7 @@ repo.transfer.body=Ja vēlaties to noraidīt vai apstiprināt, tad apmeklējiet repo.collaborator.added.subject=%s pievienoja Jūs repozitorijam %s repo.collaborator.added.text=Jūs tikāt pievienots kā līdzstrādnieks repozitorijam: + [modal] yes=Jā no=Nē diff --git a/options/locale/locale_ml-IN.ini b/options/locale/locale_ml-IN.ini index 078b013ee1b9d..90731f6f70338 100644 --- a/options/locale/locale_ml-IN.ini +++ b/options/locale/locale_ml-IN.ini @@ -261,6 +261,7 @@ register_success=രജിസ്ട്രേഷൻ വിജയകരം + [modal] yes=അതെ no=ഇല്ല diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index e3f10451f47cd..0320efbf2206b 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -407,6 +407,7 @@ repo.transfer.body=Om het te accepteren of afwijzen, bezoek %s of negeer het gew repo.collaborator.added.subject=%s heeft jou toegevoegd aan %s repo.collaborator.added.text=U bent toegevoegd als een medewerker van de repository: + [modal] yes=Ja no=Nee diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index e5e5f3186141d..005b9bdb201cd 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -392,6 +392,7 @@ repo.transfer.body=Aby zaakceptować lub odrzucić go, odwiedź %s lub po prostu repo.collaborator.added.subject=%s dodał Cię do %s repo.collaborator.added.text=Zostałeś dodany jako współtwórca repozytorium: + [modal] yes=Tak no=Nie diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 04b9a43519554..86e375c6ba15e 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -409,6 +409,7 @@ repo.transfer.body=Para o aceitar ou rejeitar visite %s, ou simplesmente o ignor repo.collaborator.added.subject=%s adicionou você a %s repo.collaborator.added.text=Você foi adicionado como um colaborador do repositório: + [modal] yes=Sim no=Não diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 6f99e4d6e8e40..14f6a33ca2765 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -412,6 +412,11 @@ repo.transfer.body=Para o aceitar ou rejeitar visite %s, ou ignore-o, simplesmen repo.collaborator.added.subject=%s adicionou você a %s repo.collaborator.added.text=Foi adicionado(a) como colaborador(a) do repositório: +team_invite.subject=%[1]s fez-lhe um convite para se juntar à organização %[2]s +team_invite.text_1=%[1]s fez-lhe um convite para se juntar à equipa %[2]s na organização %[3]s. +team_invite.text_2=Clique na ligação seguinte para se juntar à equipa: +team_invite.text_3=Nota: Este convite é dirigido a %[1]s. Se não estava à espera deste convite, pode ignorar este email. + [modal] yes=Sim no=Não @@ -487,6 +492,7 @@ user_not_exist=O utilizador não existe. team_not_exist=A equipa não existe. last_org_owner=Não pode remover o último utilizador da equipa 'proprietários'. Tem que haver pelo menos um proprietário numa organização. cannot_add_org_to_team=Uma organização não pode ser adicionada como membro de uma equipa. +duplicate_invite_to_team=O(A) utilizador(a) já tinha sido convidado(a) para ser membro da equipa. invalid_ssh_key=Não é possível validar a sua chave SSH: %s invalid_gpg_key=Não é possível validar a sua chave GPG: %s @@ -2402,6 +2408,8 @@ teams.members=Membros da equipa teams.update_settings=Modificar configurações teams.delete_team=Eliminar equipa teams.add_team_member=Adicionar membro da equipa +teams.invite_team_member=Convidar para %s +teams.invite_team_member.list=Convites pendentes teams.delete_team_title=Eliminar equipa teams.delete_team_desc=Eliminar uma equipa revoga o acesso dos seus membros ao repositório. Quer continuar? teams.delete_team_success=A equipa foi eliminada. @@ -2426,6 +2434,9 @@ teams.all_repositories_helper=A equipa tem acesso a todos os repositórios. Esco teams.all_repositories_read_permission_desc=Esta equipa atribui o acesso de leitura a todos os repositórios: os seus membros podem ver e clonar os repositórios. teams.all_repositories_write_permission_desc=Esta equipa atribui o acesso de escrita a todos os repositórios: os seus membros podem ler de, e enviar para os repositórios. teams.all_repositories_admin_permission_desc=Esta equipa atribui o acesso de administração a todos os repositórios: os seus membros podem ler de, enviar para, e adicionar colaboradores aos repositórios. +teams.invite.title=Foi-lhe feito um convite para se juntar à equipa %s na organização%s. +teams.invite.by=Convidado(a) por %s +teams.invite.description=Clique no botão abaixo para se juntar à equipa. [admin] dashboard=Painel de controlo diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index fab3dfc6d923e..f83ccb73ed2c9 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -403,6 +403,7 @@ repo.transfer.body=Для того чтобы принять или отклон repo.collaborator.added.subject=%s добавил вас в %s repo.collaborator.added.text=Вы были добавлены в качестве соавтора репозитория: + [modal] yes=Да no=Нет diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 4ddf97356c695..6652c43f22ee6 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -365,6 +365,7 @@ repo.transfer.body=එය පිළිගැනීමට හෝ ප්රති repo.collaborator.added.subject=%s ඔබව %s ට එකතු කළා repo.collaborator.added.text=ඔබ ගබඩාවේ සහයෝගිතාකරුවෙකු ලෙස එකතු කර ඇත: + [modal] yes=ඔව් no=නැහැ diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index 597b91cb1fcc3..1e0f9dc006d3f 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -409,6 +409,7 @@ repo.transfer.body=Ak to chcete prijať alebo odmietnuť, navštívte %s alebo t repo.collaborator.added.subject=%s vás pridal do %s repo.collaborator.added.text=Boli ste pridaný ako spolupracovník repozitára: + [modal] yes=Áno no=Nie diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 08733692b5d11..12d6693bde86b 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -304,6 +304,7 @@ register_success=Registreringen lyckades + [modal] yes=Ja no=Nej diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index ea489628564f8..d6eb62acaa45c 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -412,6 +412,7 @@ repo.transfer.body=Kabul veya reddetmek için %s ziyaret edin veya görmezden ge repo.collaborator.added.subject=%s sizi %s ekledi repo.collaborator.added.text=Bu depo için katkıcı olarak eklendiniz: + [modal] yes=Evet no=Hayır diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index a98f35dd7f799..e88e9cf14bfe0 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -382,6 +382,7 @@ repo.transfer.body=Щоб прийняти або відхилити перей repo.collaborator.added.subject=%s додав вас до %s repo.collaborator.added.text=Ви були додані в якості співавтора репозиторію: + [modal] yes=Так no=Ні diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 99202b9e1723e..3e009b5d2bfd4 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -412,6 +412,11 @@ repo.transfer.body=访问 %s 以接受或拒绝转移,亦可忽略此邮件。 repo.collaborator.added.subject=%s 把你添加到了 %s repo.collaborator.added.text=您已被添加为代码库的协作者: +team_invite.subject=%[1]s 邀请您加入组织 %[2]s +team_invite.text_1=%[1]s 邀请您加入组织 %[3]s 中的团队 %[2]s。 +team_invite.text_2=请点击下面的链接加入团队: +team_invite.text_3=注意:这是发送给 %[1]s 的邀请。如果您未曾收到过此类邀请,请忽略这封电子邮件。 + [modal] yes=确认操作 no=取消操作 @@ -487,6 +492,7 @@ user_not_exist=该用户不存在 team_not_exist=团队不存在 last_org_owner=您不能从 "所有者" 团队中删除最后一个用户。组织中必须至少有一个所有者。 cannot_add_org_to_team=组织不能被加入到团队中。 +duplicate_invite_to_team=此用户已被邀请为团队成员。 invalid_ssh_key=无法验证您的 SSH 密钥: %s invalid_gpg_key=无法验证您的 GPG 密钥: %s @@ -861,8 +867,8 @@ readme_helper_desc=这是您可以为您的项目撰写完整描述的地方。 auto_init=初始化仓库(添加. gitignore、许可证和自述文件) trust_model_helper=选择签名验证的“信任模型”。可能的选项是: trust_model_helper_collaborator=协作者:信任协作者的签名 -trust_model_helper_committer=提交者:信任匹配提交者的签名 -trust_model_helper_collaborator_committer=协作者+提交者:信任与提交者匹配的协作者的签名 +trust_model_helper_committer=提交者:信任与提交者相符的签名 +trust_model_helper_collaborator_committer=协作者+提交者:信任协作者同时是提交者的签名 trust_model_helper_default=默认:使用此安装的默认信任模型 create_repo=创建仓库 default_branch=默认分支 @@ -1043,7 +1049,7 @@ file_view_raw=查看原始文件 file_permalink=永久链接 file_too_large=文件过大,无法显示。 invisible_runes_header=`此文件包含不可见的 Unicode 字符!` -invisible_runes_description=`这个文件包含不可见的 Unicode 字符,其处理方式可能不同于下面显示的字符。 如果您是有意且正当地使用它们,您可以安全地忽略这个警告。使用 Escape 按钮来显示隐藏的字符。 +invisible_runes_description=`这个文件包含不可见的 Unicode 字符,其处理方式可能不同于下面显示的字符。 如果您是有意且正当地使用它们,您可以安全地忽略这个警告。使用 Escape 按钮来显示隐藏的字符。` ambiguous_runes_header=`此行包含模棱两可的 Unicode 字符!` ambiguous_runes_description=`此文件包含模棱两可的 Unicode 字符,这些字符可能会与您当前语言环境的其他字符混淆。 如果您是有意且正当地使用它们,您可以安全地忽略这个警告。使用 Escape 按钮来高亮这些字符。` invisible_runes_line=`此行含有不可见的 unicode 字符` @@ -1882,13 +1888,13 @@ settings.trust_model.default=默认信任模型 settings.trust_model.default.desc=为此安装使用默认仓库信任模型。 settings.trust_model.collaborator=协作者 settings.trust_model.collaborator.long=协作者:信任协作者的签名 -settings.trust_model.collaborator.desc=此仓库中协作者的有效签名将被标记为“可信” - 不管他们是否是提交者。否则,如果签名匹配了提交者,有效的签名将被标记为“不可信”。 +settings.trust_model.collaborator.desc=此仓库中协作者的有效签名将被标记为「可信」(无论它们是否是提交者),签名只符合提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。 settings.trust_model.committer=提交者 -settings.trust_model.committer.long=提交者: 信任与提交者匹配的签名 (匹配GitHub 并强制Gitea签名的提交者将Gitea作为提交者) -settings.trust_model.committer.desc=有效的签名只有与提交者匹配时才会被标记为“可信”,否则会被标记为“不匹配”。这会强制Gitea成为已签名提交的提交者,而实际提交者在提交中被标记为Co-authored-by: 和Co-committed-by: trailer。默认的Gitea密钥必须与数据库中的一位用户相匹配。 +settings.trust_model.committer.long=提交者: 信任与提交者相符的签名 (此特性类似 GitHub,这会强制采用 Gitea 作为提交者和签名者) +settings.trust_model.committer.desc=提交者的有效签名将被标记为「可信」,否则将被标记为「不匹配」。这会强制 Gitea 成为签名者和提交者,实际的提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Gitea 签名密钥必须匹配数据库中的一个用户密钥。 settings.trust_model.collaboratorcommitter=协作者+提交者 settings.trust_model.collaboratorcommitter.long=协作者+提交者:信任协作者同时是提交者的签名 -settings.trust_model.collaboratorcommitter.desc=如果匹配为提交者,此仓库中协作者的有效签名将被标记为“可信”。否则,如果签名匹配了提交者或者未匹配,有效的签名将被标记为“不可信”。这将强制 Gitea 在签名提交上将实际提交者加上 Co-Authored-By: 和 Co-Committed-By: 。默认的Gitea密钥必须匹配Gitea用户。 +settings.trust_model.collaboratorcommitter.desc=此仓库中协作者的有效签名在他同时是提交者时将被标记为「可信」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这会强制 Gitea 成为签名者和提交者,实际的提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Gitea 签名密钥必须匹配数据库中的一个用户密钥。 settings.wiki_delete=删除百科数据 settings.wiki_delete_desc=删除仓库百科数据是永久性的,无法撤消。 settings.wiki_delete_notices_1=- 这将永久删除和禁用 %s 的百科。 @@ -2402,6 +2408,8 @@ teams.members=团队成员 teams.update_settings=更新团队设置 teams.delete_team=删除团队 teams.add_team_member=添加团队成员 +teams.invite_team_member=邀请加入 %s +teams.invite_team_member.list=待处理的邀请 teams.delete_team_title=删除团队 teams.delete_team_desc=删除一个团队将删除团队成员的访问权限,继续? teams.delete_team_success=该团队已被删除。 @@ -2426,6 +2434,9 @@ teams.all_repositories_helper=团队可以访问所有仓库。选择此选项 teams.all_repositories_read_permission_desc=此团队授予读取所有仓库的访问权限: 成员可以查看和克隆仓库。 teams.all_repositories_write_permission_desc=此团队授予修改所有仓库的访问权限: 成员可以查看和推送至仓库。 teams.all_repositories_admin_permission_desc=该团队拥有 管理 所有仓库的权限:团队成员可以读取、克隆、推送以及添加其它仓库协作者。 +teams.invite.title=您已被邀请加入组织 %s 中的团队 %s。 +teams.invite.by=邀请人 %s +teams.invite.description=请点击下面的按钮加入团队。 [admin] dashboard=管理面板 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index ba828365d5a51..cb241d5ef4811 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -124,6 +124,7 @@ register_success=註冊成功 + [modal] yes=確認操作 no=取消操作 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 0d6e6f617d967..5827d0b2070d5 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -410,6 +410,7 @@ repo.transfer.body=請造訪 %s 以接受或拒絕轉移,您也可以忽略它 repo.collaborator.added.subject=%s 把您加入到 %s repo.collaborator.added.text=您已被新增為儲存庫的協作者: + [modal] yes=是 no=否 diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 8a4c12d57b573..b48bdb995177f 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -6,6 +6,7 @@ package auth import ( + "errors" "fmt" "net/http" "strings" @@ -24,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/utils" @@ -619,7 +621,9 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth. // update external user information if gothUser != nil { if err := externalaccount.UpdateExternalUser(u, *gothUser); err != nil { - log.Error("UpdateExternalUser failed: %v", err) + if !errors.Is(err, util.ErrNotExist) { + log.Error("UpdateExternalUser failed: %v", err) + } } } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index c172215b903d1..e0e3c6e59f740 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -1068,7 +1068,9 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model // update external user information if err := externalaccount.UpdateExternalUser(u, gothUser); err != nil { - log.Error("UpdateExternalUser failed: %v", err) + if !errors.Is(err, util.ErrNotExist) { + log.Error("UpdateExternalUser failed: %v", err) + } } if err := resetLocale(ctx, u); err != nil { diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 13c88565c4767..399d07fe47b90 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -14,7 +14,7 @@ import ( "code.gitea.io/gitea/models" "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" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/services/forms" @@ -38,6 +39,8 @@ const ( tplTeamMembers base.TplName = "org/team/members" // tplTeamRepositories template path for showing team repositories page tplTeamRepositories base.TplName = "org/team/repositories" + // tplTeamInvite template path for team invites page + tplTeamInvite base.TplName = "org/team/invite" ) // Teams render teams list page @@ -59,12 +62,6 @@ func Teams(ctx *context.Context) { // TeamsAction response for join, leave, remove, add operations to team func TeamsAction(ctx *context.Context) { - uid := ctx.FormInt64("uid") - if uid == 0 { - ctx.Redirect(ctx.Org.OrgLink + "/teams") - return - } - page := ctx.FormString("page") var err error switch ctx.Params(":action") { @@ -77,7 +74,7 @@ func TeamsAction(ctx *context.Context) { case "leave": err = models.RemoveTeamMember(ctx.Org.Team, ctx.Doer.ID) if err != nil { - if organization.IsErrLastOrgOwner(err) { + if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) } else { log.Error("Action(%s): %v", ctx.Params(":action"), err) @@ -98,9 +95,16 @@ func TeamsAction(ctx *context.Context) { ctx.Error(http.StatusNotFound) return } + + uid := ctx.FormInt64("uid") + if uid == 0 { + ctx.Redirect(ctx.Org.OrgLink + "/teams") + return + } + err = models.RemoveTeamMember(ctx.Org.Team, uid) if err != nil { - if organization.IsErrLastOrgOwner(err) { + if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) } else { log.Error("Action(%s): %v", ctx.Params(":action"), err) @@ -126,10 +130,23 @@ func TeamsAction(ctx *context.Context) { u, err = user_model.GetUserByName(ctx, uname) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Flash.Error(ctx.Tr("form.user_not_exist")) + if setting.MailService != nil && user_model.ValidateEmail(uname) == nil { + if err := org_service.CreateTeamInvite(ctx, ctx.Doer, ctx.Org.Team, uname); 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 { + ctx.Flash.Error(ctx.Tr("form.user_not_exist")) + } ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) } else { - ctx.ServerError(" GetUserByName", err) + ctx.ServerError("GetUserByName", err) } return } @@ -146,11 +163,30 @@ func TeamsAction(ctx *context.Context) { 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" } if err != nil { - if organization.IsErrLastOrgOwner(err) { + if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) } else { log.Error("Action(%s): %v", ctx.Params(":action"), err) @@ -224,7 +260,7 @@ func NewTeam(ctx *context.Context) { ctx.Data["Title"] = ctx.Org.Organization.FullName ctx.Data["PageIsOrgTeams"] = true ctx.Data["PageIsOrgTeamsNew"] = true - ctx.Data["Team"] = &organization.Team{} + ctx.Data["Team"] = &org_model.Team{} ctx.Data["Units"] = unit_model.Units ctx.HTML(http.StatusOK, tplTeamNew) } @@ -255,7 +291,7 @@ func NewTeamPost(ctx *context.Context) { p = unit_model.MinUnitAccessMode(unitPerms) } - t := &organization.Team{ + t := &org_model.Team{ OrgID: ctx.Org.Organization.ID, Name: form.TeamName, Description: form.Description, @@ -265,9 +301,9 @@ func NewTeamPost(ctx *context.Context) { } 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 { - units = append(units, &organization.TeamUnit{ + units = append(units, &org_model.TeamUnit{ OrgID: ctx.Org.Organization.ID, Type: tp, AccessMode: perm, @@ -295,7 +331,7 @@ func NewTeamPost(ctx *context.Context) { if err := models.NewTeam(t); err != nil { ctx.Data["Err_TeamName"] = true switch { - case organization.IsErrTeamAlreadyExist(err): + case org_model.IsErrTeamAlreadyExist(err): ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) default: ctx.ServerError("NewTeam", err) @@ -316,6 +352,15 @@ func TeamMembers(ctx *context.Context) { return } 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) } @@ -339,7 +384,7 @@ func SearchTeam(ctx *context.Context) { 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 Keyword: ctx.FormTrim("q"), OrgID: ctx.Org.Organization.ID, @@ -347,7 +392,7 @@ func SearchTeam(ctx *context.Context) { ListOptions: listOptions, } - teams, maxResults, err := organization.SearchTeam(opts) + teams, maxResults, err := org_model.SearchTeam(opts) if err != nil { log.Error("SearchTeam failed: %v", err) ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ @@ -424,16 +469,16 @@ func EditTeamPost(ctx *context.Context) { t.Description = form.Description 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 { - units = append(units, organization.TeamUnit{ + units = append(units, org_model.TeamUnit{ OrgID: t.OrgID, TeamID: t.ID, Type: tp, 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()) return } @@ -452,7 +497,7 @@ func EditTeamPost(ctx *context.Context) { if err := models.UpdateTeam(t, isAuthChanged, isIncludeAllChanged); err != nil { ctx.Data["Err_TeamName"] = true switch { - case organization.IsErrTeamAlreadyExist(err): + case org_model.IsErrTeamAlreadyExist(err): ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) default: ctx.ServerError("UpdateTeam", err) @@ -474,3 +519,72 @@ func DeleteTeam(ctx *context.Context) { "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 +} diff --git a/routers/web/web.go b/routers/web/web.go index 8859ec585079d..62503b3141e8d 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -651,6 +651,11 @@ func RegisterRoutes(m *web.Route) { 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.Get("/dashboard", user.Dashboard) m.Get("/dashboard/{team}", user.Dashboard) diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index e7362cdcdddcd..e687d9ae9163b 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1235,8 +1235,13 @@ func SyncAndGetUserSpecificDiff(ctx context.Context, userID int64, pull *issues_ } changedFiles, err := gitRepo.GetFilesChangedBetween(review.CommitSHA, latestCommit) + // There are way too many possible errors. + // Examples are various git errors such as the commit the review was based on was gc'ed and hence doesn't exist anymore as well as unrecoverable errors where we should serve a 500 response + // Due to the current architecture and physical limitation of needing to compare explicit error messages, we can only choose one approach without the code getting ugly + // For SOME of the errors such as the gc'ed commit, it would be best to mark all files as changed + // But as that does not work for all potential errors, we simply mark all files as unchanged and drop the error which always works, even if not as good as possible if err != nil { - return diff, err + log.Error("Could not get changed files between %s and %s for pull request %d in repo with path %s. Assuming no changes. Error: %w", review.CommitSHA, latestCommit, pull.Index, gitRepo.Path, err) } filesChangedSinceLastDiff := make(map[string]pull_model.ViewedState) diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go index 7c44f93929ec8..6df3fbbf1d0d6 100644 --- a/services/mailer/mail_release.go +++ b/services/mailer/mail_release.go @@ -23,7 +23,7 @@ const ( 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) { if setting.MailService == nil { // No mail service configured diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go new file mode 100644 index 0000000000000..c2b2a00e76097 --- /dev/null +++ b/services/mailer/mail_team_invite.go @@ -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 +} diff --git a/services/org/team_invite.go b/services/org/team_invite.go new file mode 100644 index 0000000000000..1108a46da5b45 --- /dev/null +++ b/services/org/team_invite.go @@ -0,0 +1,23 @@ +// 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 org + +import ( + "context" + + org_model "code.gitea.io/gitea/models/organization" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/services/mailer" +) + +// CreateTeamInvite make a persistent invite in db and mail it +func CreateTeamInvite(ctx context.Context, inviter *user_model.User, team *org_model.Team, uname string) error { + invite, err := org_model.CreateTeamInvite(ctx, inviter, team, uname) + if err != nil { + return err + } + + return mailer.MailTeamInvite(ctx, inviter, team, invite) +} diff --git a/templates/mail/team_invite.tmpl b/templates/mail/team_invite.tmpl new file mode 100644 index 0000000000000..163c950e94178 --- /dev/null +++ b/templates/mail/team_invite.tmpl @@ -0,0 +1,16 @@ + + +
+ + + +{{$invite_url := printf "%sorg/invite/%s" AppUrl (QueryEscape .Invite.Token)}} + +{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName) | Str2html}}
+{{.locale.Tr "mail.team_invite.text_2"}}
+{{.locale.Tr "mail.link_not_working_do_paste"}}
+{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}
+ + + + diff --git a/templates/org/team/invite.tmpl b/templates/org/team/invite.tmpl new file mode 100644 index 0000000000000..a696d99498007 --- /dev/null +++ b/templates/org/team/invite.tmpl @@ -0,0 +1,23 @@ +{{template "base/head" .}} +