diff --git a/models/auth/source.go b/models/auth/source.go index f360ca9801..a3a250cd91 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -210,7 +210,7 @@ func CreateSource(ctx context.Context, source *Source) error { return ErrSourceAlreadyExist{source.Name} } // Synchronization is only available with LDAP for now - if !source.IsLDAP() { + if !source.IsLDAP() && !source.IsOAuth2() { source.IsSyncEnabled = false } diff --git a/models/user/external_login_user.go b/models/user/external_login_user.go index 965b7a5ed1..0e764efb9f 100644 --- a/models/user/external_login_user.go +++ b/models/user/external_login_user.go @@ -160,12 +160,34 @@ func UpdateExternalUserByExternalID(ctx context.Context, external *ExternalLogin return err } +// EnsureLinkExternalToUser link the external user to the user +func EnsureLinkExternalToUser(ctx context.Context, external *ExternalLoginUser) error { + has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{ + "external_id": external.ExternalID, + "login_source_id": external.LoginSourceID, + }) + if err != nil { + return err + } + + if has { + _, err = db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", external.ExternalID, external.LoginSourceID).AllCols().Update(external) + return err + } + + _, err = db.GetEngine(ctx).Insert(external) + return err +} + // FindExternalUserOptions represents an options to find external users type FindExternalUserOptions struct { db.ListOptions - Provider string - UserID int64 - OrderBy string + Provider string + UserID int64 + LoginSourceID int64 + HasRefreshToken bool + Expired bool + OrderBy string } func (opts FindExternalUserOptions) ToConds() builder.Cond { @@ -176,9 +198,22 @@ func (opts FindExternalUserOptions) ToConds() builder.Cond { if opts.UserID > 0 { cond = cond.And(builder.Eq{"user_id": opts.UserID}) } + if opts.Expired { + cond = cond.And(builder.Lt{"expires_at": time.Now()}) + } + if opts.HasRefreshToken { + cond = cond.And(builder.Neq{"refresh_token": ""}) + } + if opts.LoginSourceID != 0 { + cond = cond.And(builder.Eq{"login_source_id": opts.LoginSourceID}) + } return cond } func (opts FindExternalUserOptions) ToOrders() string { return opts.OrderBy } + +func IterateExternalLogin(ctx context.Context, opts FindExternalUserOptions, f func(ctx context.Context, u *ExternalLoginUser) error) error { + return db.Iterate(ctx, opts.ToConds(), f) +} diff --git a/modules/indexer/issues/db/db.go b/modules/indexer/issues/db/db.go index 05ec548435..6c9cfcf670 100644 --- a/modules/indexer/issues/db/db.go +++ b/modules/indexer/issues/db/db.go @@ -71,6 +71,12 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( )), ), ) + + if options.IsKeywordNumeric() { + cond = cond.Or( + builder.Eq{"`index`": options.Keyword}, + ) + } } opt, err := ToDBOptions(ctx, options) diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 1cb86feb82..c82dc0867e 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -283,9 +283,9 @@ const ( func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) { indexer := *globalIndexer.Load() - if opts.Keyword == "" { + if opts.Keyword == "" || opts.IsKeywordNumeric() { // This is a conservative shortcut. - // If the keyword is empty, db has better (at least not worse) performance to filter issues. + // If the keyword is empty or an integer, db has better (at least not worse) performance to filter issues. // When the keyword is empty, it tends to listing rather than searching issues. // So if the user creates an issue and list issues immediately, the issue may not be listed because the indexer needs time to index the issue. // Even worse, the external indexer like elastic search may not be available for a while, diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index e426229f78..4986d8d294 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -31,6 +31,7 @@ func TestDBSearchIssues(t *testing.T) { InitIssueIndexer(true) t.Run("search issues with keyword", searchIssueWithKeyword) + t.Run("search issues by index", searchIssueByIndex) t.Run("search issues in repo", searchIssueInRepo) t.Run("search issues by ID", searchIssueByID) t.Run("search issues is pr", searchIssueIsPull) @@ -87,6 +88,43 @@ func searchIssueWithKeyword(t *testing.T) { } } +func searchIssueByIndex(t *testing.T) { + tests := []struct { + opts SearchOptions + expectedIDs []int64 + }{ + { + SearchOptions{ + Keyword: "1000", + RepoIDs: []int64{1}, + }, + []int64{}, + }, + { + SearchOptions{ + Keyword: "2", + RepoIDs: []int64{1, 2, 3, 32}, + }, + []int64{17, 12, 7, 2}, + }, + { + SearchOptions{ + Keyword: "1", + RepoIDs: []int64{58}, + }, + []int64{19}, + }, + } + + for _, test := range tests { + issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, test.expectedIDs, issueIDs) + } +} + func searchIssueInRepo(t *testing.T) { tests := []struct { opts SearchOptions diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 2dfee8b72e..a43c6be005 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -4,6 +4,8 @@ package internal import ( + "strconv" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" @@ -124,6 +126,12 @@ func (o *SearchOptions) Copy(edit ...func(options *SearchOptions)) *SearchOption return &v } +// used for optimized issue index based search +func (o *SearchOptions) IsKeywordNumeric() bool { + _, err := strconv.Atoi(o.Keyword) + return err == nil +} + type SortBy string const ( diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index eeaf1453f1..49c6fdc84d 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1927,6 +1927,7 @@ milestones.edit_success=Milník „%s“ byl aktualizován. milestones.deletion=Smazat milník milestones.deletion_desc=Odstranění milníku jej smaže ze všech souvisejících úkolů. Pokračovat? milestones.deletion_success=Milník byl odstraněn. +milestones.filter_sort.name=Název milestones.filter_sort.earliest_due_data=Nejstarší datum dokončení milestones.filter_sort.latest_due_date=Nejnovější datum dokončení milestones.filter_sort.least_complete=Nejméně dokončené diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index d9e40ebc95..d8bc62711b 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1907,6 +1907,7 @@ milestones.edit_success=Meilenstein "%s" wurde aktualisiert. milestones.deletion=Meilenstein löschen milestones.deletion_desc=Das Löschen des Meilensteins entfernt ihn von allen Issues. Fortfahren? milestones.deletion_success=Der Meilenstein wurde gelöscht. +milestones.filter_sort.name=Name milestones.filter_sort.earliest_due_data=Frühestes Fälligkeitsdatum milestones.filter_sort.latest_due_date=Spätestes Fälligkeitsdatum milestones.filter_sort.least_complete=Am wenigsten vollständig diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 15b6950be6..1c5c9227ce 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -1826,6 +1826,7 @@ milestones.edit_success=Το ορόσημο "%s" ενημερώθηκε. milestones.deletion=Διαγραφή Ορόσημου milestones.deletion_desc=Η διαγραφή ενός ορόσημου το αφαιρεί από όλα τα συναφή ζητήματα. Συνέχεια; milestones.deletion_success=Το ορόσημο έχει διαγραφεί. +milestones.filter_sort.name=Όνομα milestones.filter_sort.earliest_due_data=Πλησιέστερη παράδοση milestones.filter_sort.latest_due_date=Απώτερη παράδοση milestones.filter_sort.least_complete=Λιγότερο πλήρη diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 5d8fd1d3f1..18fd2c17f2 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1812,6 +1812,7 @@ milestones.edit_success=Se ha actualizado el hito "%s". milestones.deletion=Eliminar hito milestones.deletion_desc=Eliminando un hito lo elimina de todos los problemas relacionados. ¿Continuar? milestones.deletion_success=El hito se ha eliminado. +milestones.filter_sort.name=Nombre milestones.filter_sort.earliest_due_data=Fecha de vencimiento más temprana milestones.filter_sort.latest_due_date=Fecha de vencimiento más lejana milestones.filter_sort.least_complete=Menos completa diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index f77fad8305..9c35d99a8b 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -1389,6 +1389,7 @@ milestones.modify=به روزرسانی نقطه عطف milestones.deletion=حذف نقطه عطف milestones.deletion_desc=نقاط عطف از تمام مسائل مرتبط حذف میشوند. آیا ادامه میدهید؟ milestones.deletion_success=نقطه عطف حذف شد. +milestones.filter_sort.name=نام milestones.filter_sort.least_complete=حداقل کامل شده milestones.filter_sort.most_complete=بیشترین کامل شده milestones.filter_sort.most_issues=بیشترین مسائل diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 6d0d41ee68..4563612040 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -1009,6 +1009,7 @@ milestones.clear=Tyhjennä milestones.edit=Muokkaa merkkipaalua milestones.cancel=Peruuta milestones.modify=Päivitä merkkipaalu +milestones.filter_sort.name=Nimi milestones.filter_sort.most_issues=Eniten ongelmia milestones.filter_sort.least_issues=Vähiten ongelmia diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index efceb8d9ce..7a04ab4843 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1927,6 +1927,7 @@ milestones.edit_success=Le jalon "%s" a été mis à jour. milestones.deletion=Supprimer un Jalon milestones.deletion_desc=Supprimer un jalon le retire de tous les tickets. Continuer ? milestones.deletion_success=Le jalon a été supprimé. +milestones.filter_sort.name=Nom milestones.filter_sort.earliest_due_data=Date d’échéance la plus ancienne milestones.filter_sort.latest_due_date=Date d’échéance la plus récente milestones.filter_sort.least_complete=Le moins complété diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index fc3201426a..dbb5c7dae3 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -962,6 +962,7 @@ milestones.modify=Mérföldkő frissítése milestones.deletion=Mérföldkő törlése milestones.deletion_desc=A mérföldkő törlése eltávolítja az összes hozzárendelt hibajegyet. Biztosan folytatja? milestones.deletion_success=A mérföldkő törölve. +milestones.filter_sort.name=Név milestones.filter_sort.least_complete=Legkevésbé befejezve milestones.filter_sort.most_complete=Leginkább befejezve milestones.filter_sort.most_issues=Legtöbb hibajegy diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index ea0b2030fe..a3a6f8598e 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -770,6 +770,7 @@ milestones.due_date=Jatuh Tempo (opsional) milestones.clear=Bersihkan milestones.edit=Ubah Milestone milestones.cancel=Batal +milestones.filter_sort.name=Nama milestones.filter_sort.least_complete=Paling tidak lengkap milestones.filter_sort.most_complete=Paling lengkap milestones.filter_sort.most_issues=Paling banyak masalah diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index a75a11ba34..af71558e50 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -916,6 +916,7 @@ milestones.desc=Lýsing milestones.due_date=Eindagi (valfrjálst) milestones.clear=Hreinsa milestones.cancel=Hætta við +milestones.filter_sort.name=Heiti milestones.filter_sort.most_issues=Flest vandamál milestones.filter_sort.least_issues=Fæst vandamál diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 79a469d248..d9acd9f44a 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -1512,6 +1512,7 @@ milestones.modify=Aggiorna pietra miliare milestones.deletion=Elimina pietra miliare milestones.deletion_desc=Eliminare una pietra miliare la rimuove da tutte le relative issue. Continuare? milestones.deletion_success=La pietra miliare è stata eliminata. +milestones.filter_sort.name=Nome milestones.filter_sort.least_complete=Meno completato milestones.filter_sort.most_complete=Più completato milestones.filter_sort.most_issues=Più problemi diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 64f44ac98b..2cdae3e47d 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -1933,6 +1933,7 @@ milestones.edit_success=マイルストーン "%s" を更新しました。 milestones.deletion=マイルストーンの削除 milestones.deletion_desc=マイルストーンを削除すると、関連するすべてのイシューから除去されます。 続行しますか? milestones.deletion_success=マイルストーンを削除しました。 +milestones.filter_sort.name=名称 milestones.filter_sort.earliest_due_data=期日が早い順 milestones.filter_sort.latest_due_date=期日が遅い順 milestones.filter_sort.least_complete=消化率の低い順 diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 8b7b44588e..16f4bd310e 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -875,6 +875,7 @@ milestones.modify=마일스톤 갱신 milestones.deletion=마일스톤 삭제 milestones.deletion_desc=마일스톤을 삭제하면 연관된 모든 이슈에서 삭제됩니다. 계속 하시겠습니까? milestones.deletion_success=마일스톤이 삭제되었습니다. +milestones.filter_sort.name=이름 milestones.filter_sort.least_complete=완료율이 낮은 순 milestones.filter_sort.most_complete=완료율이 높은 순 milestones.filter_sort.most_issues=이슈 많은 순 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index f2f2e3b233..a862835ba8 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -1828,6 +1828,7 @@ milestones.edit_success=Izmaiņas atskaites punktā "%s" tika veiksmīgi saglab milestones.deletion=Dzēst atskaites punktu milestones.deletion_desc=Dzēšot šo atskaites punktu, tas tiks noņemts no visām saistītajām problēmām un izmaiņu pieprasījumiem. Vai turpināt? milestones.deletion_success=Atskaites punkts tika veiksmīgi izdzēsts. +milestones.filter_sort.name=Nosaukums milestones.filter_sort.earliest_due_data=Agrākais izpildes laiks milestones.filter_sort.latest_due_date=Vēlākais izpildes laiks milestones.filter_sort.least_complete=Vismazāk pabeigtais diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index a6651ecd25..6acb314018 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -1507,6 +1507,7 @@ milestones.modify=Mijlpaal bijwerken milestones.deletion=Mijlpaal verwijderen milestones.deletion_desc=Als je een mijlpaal verwijdert, wordt hij van alle gerelateerde kwesties verwijderd. Doorgaan? milestones.deletion_success=De mijlpaal is verwijderd. +milestones.filter_sort.name=Naam milestones.filter_sort.least_complete=Minst compleet milestones.filter_sort.most_complete=Meest compleet milestones.filter_sort.most_issues=Meeste problemen diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 3028eab9ba..e6bd9ecf2a 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -1360,6 +1360,7 @@ milestones.modify=Zaktualizuj cel milestones.deletion=Usuń kamień milowy milestones.deletion_desc=Usunięcie celu usuwa go z wszystkich pozostałych zagadnień. Kontynuować? milestones.deletion_success=Cel został usunięty. +milestones.filter_sort.name=Nazwa milestones.filter_sort.least_complete=Najmniej kompletne milestones.filter_sort.most_complete=Najbardziej kompletne milestones.filter_sort.most_issues=Najwięcej zgłoszeń diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index aa183321aa..a1b0849870 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1820,6 +1820,7 @@ milestones.edit_success=O marco "%s" foi atualizado. milestones.deletion=Excluir marco milestones.deletion_desc=A exclusão deste marco irá removê-lo de todas as issues. Tem certeza que deseja continuar? milestones.deletion_success=O marco foi excluído. +milestones.filter_sort.name=Nome milestones.filter_sort.earliest_due_data=Data limite mais próxima milestones.filter_sort.latest_due_date=Data limite mais distante milestones.filter_sort.least_complete=Menos completo diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 7fbb1e0fa7..85e7a79bc9 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -477,6 +477,7 @@ activate_email=Valide o seu endereço de email activate_email.title=%s, por favor valide o seu endereço de email activate_email.text=Por favor clique na seguinte ligação para validar o seu endereço de email dentro de %s: +register_notify=Bem-vindo(a) a %s register_notify.title=%[1]s, bem-vindo(a) a %[2]s register_notify.text_1=este é o seu email de confirmação de registo para %s! register_notify.text_2=Agora pode iniciar a sessão com o nome de utilizador: %s. @@ -1933,6 +1934,7 @@ milestones.edit_success=A etapa "%s" foi modificada. milestones.deletion=Eliminar etapa milestones.deletion_desc=Se eliminar uma etapa, irá removê-la de todas as questões relacionadas. Quer continuar? milestones.deletion_success=A etapa foi eliminada. +milestones.filter_sort.name=Nome milestones.filter_sort.earliest_due_data=Data de vencimento mais próxima milestones.filter_sort.latest_due_date=Data de vencimento mais distante milestones.filter_sort.least_complete=Menos completo @@ -2384,6 +2386,7 @@ settings.protect_enable_merge=Habilitar integração settings.protect_enable_merge_desc=Qualquer pessoa com permissão de escrita tem autorização para realizar neste ramo as integrações constantes nos pedidos. settings.protect_whitelist_committers=Lista de permissões para restringir os envios settings.protect_whitelist_committers_desc=Apenas os utilizadores ou equipas constantes na lista terão permissão para enviar para este ramo (mas não poderão fazer envios forçados). +settings.protect_whitelist_deploy_keys=Lista de permissão de chaves de instalação com acesso de escrita para enviar. settings.protect_whitelist_users=Utilizadores com permissão para enviar: settings.protect_whitelist_teams=Equipas com permissão para enviar: settings.protect_force_push_allowlist_users=Utilizadores na lista de permissão para enviar forçadamente: diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 7f00d68324..9c443f7d77 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1789,6 +1789,7 @@ milestones.edit_success=Этап «%s» обновлён. milestones.deletion=Удалить этап milestones.deletion_desc=Удаление этапа приведет к его удалению из всех связанных задач. Продолжить? milestones.deletion_success=Этап успешно удалён. +milestones.filter_sort.name=Название milestones.filter_sort.earliest_due_data=По возрастанию даты завершения milestones.filter_sort.latest_due_date=По убыванию даты завершения milestones.filter_sort.least_complete=Менее полное diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 758d8386bf..8d664be3e0 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -1352,6 +1352,7 @@ milestones.modify=සන්ධිස්ථානයක් යාවත්කා milestones.deletion=සන්ධිස්ථානය මකන්න milestones.deletion_desc=සන්ධිස්ථානයක් මකා දැමීම සම්බන්ධ සියලු ගැටළු වලින් එය ඉවත් කරයි. දිගටම? milestones.deletion_success=සන්ධිස්ථානය මකා දමා ඇත. +milestones.filter_sort.name=නම milestones.filter_sort.least_complete=අවම වශයෙන් සම්පූර්ණයි milestones.filter_sort.most_complete=වඩාත්ම සම්පූර්ණයි milestones.filter_sort.most_issues=බොහෝ ප්රශ්න diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index d93700c190..11f5ed79fb 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -1132,6 +1132,7 @@ milestones.modify=Uppdatera milstolpe milestones.deletion=Ta bort milstolpe milestones.deletion_desc=Borttagning av en milstolpe tar bort den från samtliga relaterade ärende. Fortsätta? milestones.deletion_success=Milstolpen har blivit borttagen. +milestones.filter_sort.name=Namn milestones.filter_sort.least_complete=Minst klar milestones.filter_sort.most_complete=Mest klar milestones.filter_sort.most_issues=Mest ärenden diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index f52299c67f..f30b49683a 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -1920,6 +1920,7 @@ milestones.edit_success=`"%s" dönüm noktası güncellendi.` milestones.deletion=Kilometre Taşını Sil milestones.deletion_desc=Bir kilometre taşını silmek, onu ilgili tüm sorunlardan kaldırır. Devam edilsin mi? milestones.deletion_success=Kilometre taşı silindi. +milestones.filter_sort.name=İsim milestones.filter_sort.earliest_due_data=En erken bitiş tarihi milestones.filter_sort.latest_due_date=En uzak bitiş tarihi milestones.filter_sort.least_complete=En az tamamlama diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index daf0fc6948..00a1fd552e 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -1399,6 +1399,7 @@ milestones.modify=Оновити етап milestones.deletion=Видалити етап milestones.deletion_desc=Видалення етапу призведе до його видалення з усіх пов'язаних задач. Продовжити? milestones.deletion_success=Етап успішно видалено. +milestones.filter_sort.name=Назва milestones.filter_sort.least_complete=Менш повне milestones.filter_sort.most_complete=Більш повне milestones.filter_sort.most_issues=Найбільш задач diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 064c42d60a..0e8c6677e6 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1923,6 +1923,7 @@ milestones.edit_success=里程碑 %s 已经更新。 milestones.deletion=删除里程碑 milestones.deletion_desc=删除该里程碑将会移除所有工单中相关的信息。是否继续? milestones.deletion_success=里程碑已被删除。 +milestones.filter_sort.name=名称 milestones.filter_sort.earliest_due_data=到期日从远到近 milestones.filter_sort.latest_due_date=到期日从近到远 milestones.filter_sort.least_complete=完成度从低到高 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index ceb9e2844d..64a012e307 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -511,6 +511,7 @@ milestones.due_date=截止日期(可選) milestones.clear=清除 milestones.edit=編輯里程碑 milestones.cancel=取消 +milestones.filter_sort.name=組織名稱 milestones.filter_sort.least_complete=完成度由低到高 milestones.filter_sort.most_complete=完成度由高到低 milestones.filter_sort.most_issues=問題由多到少 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index b278639c2e..071cc76f68 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1656,6 +1656,7 @@ milestones.edit_success=已更新里程碑「%s」。 milestones.deletion=刪除里程碑 milestones.deletion_desc=刪除里程碑會從所有相關的問題移除它。是否繼續? milestones.deletion_success=里程碑已刪除 +milestones.filter_sort.name=名稱 milestones.filter_sort.least_complete=完成度由低到高 milestones.filter_sort.most_complete=完成度由高到低 milestones.filter_sort.most_issues=問題由多到少 diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 842020791f..15a80bc104 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -622,10 +622,8 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth. // update external user information if gothUser != nil { - if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil { - if !errors.Is(err, util.ErrNotExist) { - log.Error("UpdateExternalUser failed: %v", err) - } + if err := externalaccount.EnsureLinkExternalToUser(ctx, u, *gothUser); err != nil { + log.Error("EnsureLinkExternalToUser failed: %v", err) } } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 50f0dff2b6..1205c2c578 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -27,7 +27,6 @@ import ( "code.gitea.io/gitea/modules/optional" "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" auth_service "code.gitea.io/gitea/services/auth" @@ -1148,9 +1147,39 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model groups := getClaimedGroups(oauth2Source, &gothUser) + opts := &user_service.UpdateOptions{} + + // Reactivate user if they are deactivated + if !u.IsActive { + opts.IsActive = optional.Some(true) + } + + // Update GroupClaims + opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser) + + if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval { + if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil { + ctx.ServerError("SyncGroupsToTeams", err) + return + } + } + + if err := externalaccount.EnsureLinkExternalToUser(ctx, u, gothUser); err != nil { + ctx.ServerError("EnsureLinkExternalToUser", err) + return + } + // If this user is enrolled in 2FA and this source doesn't override it, // we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page. if !needs2FA { + // Register last login + opts.SetLastLogin = true + + if err := user_service.UpdateUser(ctx, u, opts); err != nil { + ctx.ServerError("UpdateUser", err) + return + } + if err := updateSession(ctx, nil, map[string]any{ "uid": u.ID, "uname": u.Name, @@ -1162,29 +1191,6 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model // Clear whatever CSRF cookie has right now, force to generate a new one ctx.Csrf.DeleteCookie(ctx) - opts := &user_service.UpdateOptions{ - SetLastLogin: true, - } - opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser) - if err := user_service.UpdateUser(ctx, u, opts); err != nil { - ctx.ServerError("UpdateUser", err) - return - } - - if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval { - if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil { - ctx.ServerError("SyncGroupsToTeams", err) - return - } - } - - // update external user information - if err := externalaccount.UpdateExternalUser(ctx, u, gothUser); err != nil { - if !errors.Is(err, util.ErrNotExist) { - log.Error("UpdateExternalUser failed: %v", err) - } - } - if err := resetLocale(ctx, u); err != nil { ctx.ServerError("resetLocale", err) return @@ -1200,22 +1206,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model return } - opts := &user_service.UpdateOptions{} - opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser) - if opts.IsAdmin.Has() || opts.IsRestricted.Has() { + if opts.IsActive.Has() || opts.IsAdmin.Has() || opts.IsRestricted.Has() { if err := user_service.UpdateUser(ctx, u, opts); err != nil { ctx.ServerError("UpdateUser", err) return } } - if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval { - if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil { - ctx.ServerError("SyncGroupsToTeams", err) - return - } - } - if err := updateSession(ctx, nil, map[string]any{ // User needs to use 2FA, save data and redirect to 2FA page. "twofaUid": u.ID, diff --git a/services/auth/source/oauth2/main_test.go b/services/auth/source/oauth2/main_test.go new file mode 100644 index 0000000000..57c74fd3e7 --- /dev/null +++ b/services/auth/source/oauth2/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package oauth2 + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{}) +} diff --git a/services/auth/source/oauth2/providers_test.go b/services/auth/source/oauth2/providers_test.go new file mode 100644 index 0000000000..353816c71e --- /dev/null +++ b/services/auth/source/oauth2/providers_test.go @@ -0,0 +1,62 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package oauth2 + +import ( + "time" + + "github.com/markbates/goth" + "golang.org/x/oauth2" +) + +type fakeProvider struct{} + +func (p *fakeProvider) Name() string { + return "fake" +} + +func (p *fakeProvider) SetName(name string) {} + +func (p *fakeProvider) BeginAuth(state string) (goth.Session, error) { + return nil, nil +} + +func (p *fakeProvider) UnmarshalSession(string) (goth.Session, error) { + return nil, nil +} + +func (p *fakeProvider) FetchUser(goth.Session) (goth.User, error) { + return goth.User{}, nil +} + +func (p *fakeProvider) Debug(bool) { +} + +func (p *fakeProvider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + switch refreshToken { + case "expired": + return nil, &oauth2.RetrieveError{ + ErrorCode: "invalid_grant", + } + default: + return &oauth2.Token{ + AccessToken: "token", + TokenType: "Bearer", + RefreshToken: "refresh", + Expiry: time.Now().Add(time.Hour), + }, nil + } +} + +func (p *fakeProvider) RefreshTokenAvailable() bool { + return true +} + +func init() { + RegisterGothProvider( + NewSimpleProvider("fake", "Fake", []string{"account"}, + func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider { + return &fakeProvider{} + })) +} diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go index 675005e55a..3454c9ad55 100644 --- a/services/auth/source/oauth2/source.go +++ b/services/auth/source/oauth2/source.go @@ -36,7 +36,7 @@ func (source *Source) FromDB(bs []byte) error { return json.UnmarshalHandleDoubleEncode(bs, &source) } -// ToDB exports an SMTPConfig to a serialized format. +// ToDB exports an OAuth2Config to a serialized format. func (source *Source) ToDB() ([]byte, error) { return json.Marshal(source) } diff --git a/services/auth/source/oauth2/source_sync.go b/services/auth/source/oauth2/source_sync.go new file mode 100644 index 0000000000..5e30313c8f --- /dev/null +++ b/services/auth/source/oauth2/source_sync.go @@ -0,0 +1,114 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package oauth2 + +import ( + "context" + "time" + + "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + + "github.com/markbates/goth" + "golang.org/x/oauth2" +) + +// Sync causes this OAuth2 source to synchronize its users with the db. +func (source *Source) Sync(ctx context.Context, updateExisting bool) error { + log.Trace("Doing: SyncExternalUsers[%s] %d", source.authSource.Name, source.authSource.ID) + + if !updateExisting { + log.Info("SyncExternalUsers[%s] not running since updateExisting is false", source.authSource.Name) + return nil + } + + provider, err := createProvider(source.authSource.Name, source) + if err != nil { + return err + } + + if !provider.RefreshTokenAvailable() { + log.Trace("SyncExternalUsers[%s] provider doesn't support refresh tokens, can't synchronize", source.authSource.Name) + return nil + } + + opts := user_model.FindExternalUserOptions{ + HasRefreshToken: true, + Expired: true, + LoginSourceID: source.authSource.ID, + } + + return user_model.IterateExternalLogin(ctx, opts, func(ctx context.Context, u *user_model.ExternalLoginUser) error { + return source.refresh(ctx, provider, u) + }) +} + +func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *user_model.ExternalLoginUser) error { + log.Trace("Syncing login_source_id=%d external_id=%s expiration=%s", u.LoginSourceID, u.ExternalID, u.ExpiresAt) + + shouldDisable := false + + token, err := provider.RefreshToken(u.RefreshToken) + if err != nil { + if err, ok := err.(*oauth2.RetrieveError); ok && err.ErrorCode == "invalid_grant" { + // this signals that the token is not valid and the user should be disabled + shouldDisable = true + } else { + return err + } + } + + user := &user_model.User{ + LoginName: u.ExternalID, + LoginType: auth.OAuth2, + LoginSource: u.LoginSourceID, + } + + hasUser, err := user_model.GetUser(ctx, user) + if err != nil { + return err + } + + // If the grant is no longer valid, disable the user and + // delete local tokens. If the OAuth2 provider still + // recognizes them as a valid user, they will be able to login + // via their provider and reactivate their account. + if shouldDisable { + log.Info("SyncExternalUsers[%s] disabling user %d", source.authSource.Name, user.ID) + + return db.WithTx(ctx, func(ctx context.Context) error { + if hasUser { + user.IsActive = false + err := user_model.UpdateUserCols(ctx, user, "is_active") + if err != nil { + return err + } + } + + // Delete stored tokens, since they are invalid. This + // also provents us from checking this in subsequent runs. + u.AccessToken = "" + u.RefreshToken = "" + u.ExpiresAt = time.Time{} + + return user_model.UpdateExternalUserByExternalID(ctx, u) + }) + } + + // Otherwise, update the tokens + u.AccessToken = token.AccessToken + u.ExpiresAt = token.Expiry + + // Some providers only update access tokens provide a new + // refresh token, so avoid updating it if it's empty + if token.RefreshToken != "" { + u.RefreshToken = token.RefreshToken + } + + err = user_model.UpdateExternalUserByExternalID(ctx, u) + + return err +} diff --git a/services/auth/source/oauth2/source_sync_test.go b/services/auth/source/oauth2/source_sync_test.go new file mode 100644 index 0000000000..e2f04bcb25 --- /dev/null +++ b/services/auth/source/oauth2/source_sync_test.go @@ -0,0 +1,100 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package oauth2 + +import ( + "context" + "testing" + + "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func TestSource(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + source := &Source{ + Provider: "fake", + authSource: &auth.Source{ + ID: 12, + Type: auth.OAuth2, + Name: "fake", + IsActive: true, + IsSyncEnabled: true, + }, + } + + user := &user_model.User{ + LoginName: "external", + LoginType: auth.OAuth2, + LoginSource: source.authSource.ID, + Name: "test", + Email: "external@example.com", + } + + err := user_model.CreateUser(context.Background(), user, &user_model.CreateUserOverwriteOptions{}) + assert.NoError(t, err) + + e := &user_model.ExternalLoginUser{ + ExternalID: "external", + UserID: user.ID, + LoginSourceID: user.LoginSource, + RefreshToken: "valid", + } + err = user_model.LinkExternalToUser(context.Background(), user, e) + assert.NoError(t, err) + + provider, err := createProvider(source.authSource.Name, source) + assert.NoError(t, err) + + t.Run("refresh", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + err := source.refresh(context.Background(), provider, e) + assert.NoError(t, err) + + e := &user_model.ExternalLoginUser{ + ExternalID: e.ExternalID, + LoginSourceID: e.LoginSourceID, + } + + ok, err := user_model.GetExternalLogin(context.Background(), e) + assert.NoError(t, err) + assert.True(t, ok) + assert.Equal(t, e.RefreshToken, "refresh") + assert.Equal(t, e.AccessToken, "token") + + u, err := user_model.GetUserByID(context.Background(), user.ID) + assert.NoError(t, err) + assert.True(t, u.IsActive) + }) + + t.Run("expired", func(t *testing.T) { + err := source.refresh(context.Background(), provider, &user_model.ExternalLoginUser{ + ExternalID: "external", + UserID: user.ID, + LoginSourceID: user.LoginSource, + RefreshToken: "expired", + }) + assert.NoError(t, err) + + e := &user_model.ExternalLoginUser{ + ExternalID: e.ExternalID, + LoginSourceID: e.LoginSourceID, + } + + ok, err := user_model.GetExternalLogin(context.Background(), e) + assert.NoError(t, err) + assert.True(t, ok) + assert.Equal(t, e.RefreshToken, "") + assert.Equal(t, e.AccessToken, "") + + u, err := user_model.GetUserByID(context.Background(), user.ID) + assert.NoError(t, err) + assert.False(t, u.IsActive) + }) + }) +} diff --git a/services/externalaccount/user.go b/services/externalaccount/user.go index 3cfd8c81f9..b53e33654a 100644 --- a/services/externalaccount/user.go +++ b/services/externalaccount/user.go @@ -71,14 +71,14 @@ func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth return nil } -// UpdateExternalUser updates external user's information -func UpdateExternalUser(ctx context.Context, user *user_model.User, gothUser goth.User) error { +// EnsureLinkExternalToUser link the gothUser to the user +func EnsureLinkExternalToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error { externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser) if err != nil { return err } - return user_model.UpdateExternalUserByExternalID(ctx, externalLoginUser) + return user_model.EnsureLinkExternalToUser(ctx, externalLoginUser) } // UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index e140d6b5eb..660f0d0881 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -412,7 +412,7 @@

{{ctx.Locale.Tr "admin.auths.sspi_default_language_helper"}}

{{end}} - {{if .Source.IsLDAP}} + {{if (or .Source.IsLDAP .Source.IsOAuth2)}}
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index f130e18f65..e3985cb227 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -59,7 +59,7 @@
-
+
diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index 0d733f663a..317787f403 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -224,7 +224,8 @@ func TestLDAPUserSync(t *testing.T) { } defer tests.PrepareTestEnv(t)() addAuthSourceLDAP(t, "", "") - auth.SyncExternalUsers(context.Background(), true) + err := auth.SyncExternalUsers(context.Background(), true) + assert.NoError(t, err) // Check if users exists for _, gitLDAPUser := range gitLDAPUsers { diff --git a/web_src/css/org.css b/web_src/css/org.css index 148cb975e4..90e5d7ad0e 100644 --- a/web_src/css/org.css +++ b/web_src/css/org.css @@ -96,7 +96,6 @@ .page-content.organization #org-info { overflow-wrap: anywhere; flex: 1; - word-break: break-all; } .page-content.organization #org-info .ui.header { diff --git a/web_src/css/repo.css b/web_src/css/repo.css index f34b1e7ea5..85f33f858e 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2466,7 +2466,7 @@ tbody.commit-list { .sidebar-item-link { display: inline-flex; align-items: center; - word-break: break-all; + overflow-wrap: anywhere; } .diff-file-header {