diff --git a/options/locale/locale_cs-CZ.json b/options/locale/locale_cs-CZ.json index 34a4faec94..f822be0de8 100644 --- a/options/locale/locale_cs-CZ.json +++ b/options/locale/locale_cs-CZ.json @@ -3321,5 +3321,6 @@ "git.filemode.normal_file": "Normální soubor", "git.filemode.executable_file": "Spustitelný soubor", "git.filemode.symbolic_link": "Symbolický odkaz", - "git.filemode.submodule": "Submodul" + "git.filemode.submodule": "Submodul", + "settings.oauth_applications_disabled": "OAuth aplikace jsou v m8sh zakázány, použijte osobní přístupový token." } \ No newline at end of file diff --git a/options/locale/locale_de-DE.json b/options/locale/locale_de-DE.json index 47f5b02bd0..9d53bce2be 100644 --- a/options/locale/locale_de-DE.json +++ b/options/locale/locale_de-DE.json @@ -3263,5 +3263,6 @@ "git.filemode.normal_file": "Normale Datei", "git.filemode.executable_file": "Ausführbare Datei", "git.filemode.symbolic_link": "Softlink", - "git.filemode.submodule": "Submodul" + "git.filemode.submodule": "Submodul", + "settings.oauth_applications_disabled": "OAuth-Anwendungen sind in m8sh deaktiviert, verwenden Sie einen persönlichen Zugriffstoken." } \ No newline at end of file diff --git a/options/locale/locale_el-GR.json b/options/locale/locale_el-GR.json index 088c39e278..1bae16de3d 100644 --- a/options/locale/locale_el-GR.json +++ b/options/locale/locale_el-GR.json @@ -3015,5 +3015,6 @@ "gpg.signup.paste_key": "Επικολλήστε το δημόσιο κλειδί GPG σας", "gpg.signup.proceed": "Συνέχεια", "gpg.signup.submit": "Επαλήθευση & Δημιουργία λογαριασμού", - "gpg.signup.already_have_account": "Έχετε ήδη λογαριασμό;" + "gpg.signup.already_have_account": "Έχετε ήδη λογαριασμό;", + "settings.oauth_applications_disabled": "Οι εφαρμογές OAuth είναι απενεργοποιημένες στο m8sh, χρησιμοποιήστε προσωπικό διακριτικό πρόσβασης." } \ No newline at end of file diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 315c2b49f4..7cc46c0dd4 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3902,5 +3902,6 @@ "actions.general.cross_repo_desc": "Allow the selected repositories to be accessed (read-only) by all the repositories in this owner with GITEA_TOKEN when running Actions jobs.", "actions.general.cross_repo_selected": "Selected repositories", "actions.general.cross_repo_target_repos": "Target Repositories", - "actions.general.cross_repo_add": "Add Target Repository" + "actions.general.cross_repo_add": "Add Target Repository", + "settings.oauth_applications_disabled": "OAuth applications are disabled in m8sh, use personal access token." } \ No newline at end of file diff --git a/options/locale/locale_es-ES.json b/options/locale/locale_es-ES.json index 34bbf445e7..a8d06775f7 100644 --- a/options/locale/locale_es-ES.json +++ b/options/locale/locale_es-ES.json @@ -2974,5 +2974,6 @@ "git.filemode.normal_file": "Archivo normal", "git.filemode.executable_file": "Archivo ejecutable", "git.filemode.symbolic_link": "Enlace simbólico", - "git.filemode.submodule": "Submódulo" + "git.filemode.submodule": "Submódulo", + "settings.oauth_applications_disabled": "Las aplicaciones OAuth están deshabilitadas en m8sh, usa un token de acceso personal." } \ No newline at end of file diff --git a/options/locale/locale_fa-IR.json b/options/locale/locale_fa-IR.json index 60bef04417..c998c6f1a0 100644 --- a/options/locale/locale_fa-IR.json +++ b/options/locale/locale_fa-IR.json @@ -2243,5 +2243,6 @@ "gpg.signup.paste_key": "کلید عمومی GPG خود را جای‌گذاری کنید", "gpg.signup.proceed": "ادامه", "gpg.signup.submit": "تأیید و ایجاد حساب", - "gpg.signup.already_have_account": "قبلاً حساب دارید؟" + "gpg.signup.already_have_account": "قبلاً حساب دارید؟", + "settings.oauth_applications_disabled": "برنامه‌های OAuth در m8sh غیرفعال هستند، از توکن دسترسی شخصی استفاده کنید." } \ No newline at end of file diff --git a/options/locale/locale_fi-FI.json b/options/locale/locale_fi-FI.json index 59a3417901..2354d4db98 100644 --- a/options/locale/locale_fi-FI.json +++ b/options/locale/locale_fi-FI.json @@ -1489,5 +1489,6 @@ "gpg.signup.paste_key": "Liitä GPG-julkinen avaimesi", "gpg.signup.proceed": "Jatka", "gpg.signup.submit": "Vahvista ja luo tili", - "gpg.signup.already_have_account": "Onko sinulla jo tili?" + "gpg.signup.already_have_account": "Onko sinulla jo tili?", + "settings.oauth_applications_disabled": "OAuth-sovellukset on poistettu käytöstä m8sh-järjestelmässä, käytä henkilökohtaista käyttöoikeustunnusta." } \ No newline at end of file diff --git a/options/locale/locale_fr-FR.json b/options/locale/locale_fr-FR.json index cff5f1623d..3334222ee8 100644 --- a/options/locale/locale_fr-FR.json +++ b/options/locale/locale_fr-FR.json @@ -3880,5 +3880,6 @@ "actions.general.cross_repo_desc": "Permet aux dépôts sélectionnés d’être visible en lecture-seule par tous les dépôts de ce propriétaire à l’aide de GITEA_TOKEN lors de l’exécution des tâches d’actions.", "actions.general.cross_repo_selected": "Dépôts sélectionnés", "actions.general.cross_repo_target_repos": "Dépôts cibles", - "actions.general.cross_repo_add": "Ajouter un dépôt cible" + "actions.general.cross_repo_add": "Ajouter un dépôt cible", + "settings.oauth_applications_disabled": "Les applications OAuth sont désactivées dans m8sh, utilisez un jeton d'accès personnel." } \ No newline at end of file diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 00bdb2d361..5f257cb2da 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -3894,5 +3894,6 @@ "gpg.signup.paste_key": "Greamaigh d'eochair phoiblí GPG", "gpg.signup.proceed": "Ar aghaidh", "gpg.signup.submit": "Fíoraigh & cruthaigh cuntas", - "gpg.signup.already_have_account": "An bhfuil cuntas agat cheana?" + "gpg.signup.already_have_account": "An bhfuil cuntas agat cheana?", + "settings.oauth_applications_disabled": "Tá feidhmchláir OAuth díchumasaithe i m8sh, bain úsáid as comhartha rochtana pearsanta." } \ No newline at end of file diff --git a/options/locale/locale_hu-HU.json b/options/locale/locale_hu-HU.json index 634ce3a3da..4b6bca8265 100644 --- a/options/locale/locale_hu-HU.json +++ b/options/locale/locale_hu-HU.json @@ -1398,5 +1398,6 @@ "gpg.signup.paste_key": "Illeszd be a GPG nyilvános kulcsodat", "gpg.signup.proceed": "Tovább", "gpg.signup.submit": "Ellenőrzés & fiók létrehozása", - "gpg.signup.already_have_account": "Már van fiókod?" + "gpg.signup.already_have_account": "Már van fiókod?", + "settings.oauth_applications_disabled": "Az OAuth-alkalmazások le vannak tiltva a m8sh-ban, használjon személyes hozzáférési tokent." } \ No newline at end of file diff --git a/options/locale/locale_id-ID.json b/options/locale/locale_id-ID.json index dc9724c79e..004ab7ce77 100644 --- a/options/locale/locale_id-ID.json +++ b/options/locale/locale_id-ID.json @@ -1213,5 +1213,6 @@ "gpg.signup.paste_key": "Tempel kunci publik GPG Anda", "gpg.signup.proceed": "Lanjutkan", "gpg.signup.submit": "Verifikasi & buat akun", - "gpg.signup.already_have_account": "Sudah punya akun?" + "gpg.signup.already_have_account": "Sudah punya akun?", + "settings.oauth_applications_disabled": "Aplikasi OAuth dinonaktifkan di m8sh, gunakan token akses pribadi." } \ No newline at end of file diff --git a/options/locale/locale_is-IS.json b/options/locale/locale_is-IS.json index dc1882200d..05901ca070 100644 --- a/options/locale/locale_is-IS.json +++ b/options/locale/locale_is-IS.json @@ -1131,5 +1131,6 @@ "gpg.signup.paste_key": "Límdu GPG opinbera lykilinn þinn", "gpg.signup.proceed": "Halda áfram", "gpg.signup.submit": "Staðfesta & búa til reikning", - "gpg.signup.already_have_account": "Áttu nú þegar reikning?" + "gpg.signup.already_have_account": "Áttu nú þegar reikning?", + "settings.oauth_applications_disabled": "OAuth forrit eru óvirk í m8sh, notaðu persónulegan aðgangstákn." } \ No newline at end of file diff --git a/options/locale/locale_it-IT.json b/options/locale/locale_it-IT.json index d0ae4ce3b4..0c3e1e949e 100644 --- a/options/locale/locale_it-IT.json +++ b/options/locale/locale_it-IT.json @@ -2392,5 +2392,6 @@ "gpg.signup.paste_key": "Incolla la tua chiave pubblica GPG", "gpg.signup.proceed": "Continua", "gpg.signup.submit": "Verifica e crea account", - "gpg.signup.already_have_account": "Hai già un account?" + "gpg.signup.already_have_account": "Hai già un account?", + "settings.oauth_applications_disabled": "Le applicazioni OAuth sono disabilitate in m8sh, usa un token di accesso personale." } \ No newline at end of file diff --git a/options/locale/locale_ja-JP.json b/options/locale/locale_ja-JP.json index 26a25d2137..c11c46015d 100644 --- a/options/locale/locale_ja-JP.json +++ b/options/locale/locale_ja-JP.json @@ -3865,5 +3865,6 @@ "actions.general.cross_repo_desc": "Actionsジョブの実行時に、このオーナーのすべてのリポジトリから選択したリポジトリへ、GITEA_TOKENを使用して(読み取り専用で)アクセスすることを許可します。", "actions.general.cross_repo_selected": "選択したリポジトリ", "actions.general.cross_repo_target_repos": "対象リポジトリ", - "actions.general.cross_repo_add": "対象リポジトリの追加" + "actions.general.cross_repo_add": "対象リポジトリの追加", + "settings.oauth_applications_disabled": "m8sh では OAuth アプリケーションは無効です。個人用アクセストークンを使用してください。" } \ No newline at end of file diff --git a/options/locale/locale_ko-KR.json b/options/locale/locale_ko-KR.json index 8f1b5f535f..c54d5fd901 100644 --- a/options/locale/locale_ko-KR.json +++ b/options/locale/locale_ko-KR.json @@ -3893,5 +3893,6 @@ "actions.general.cross_repo_desc": "선택된 저장소가 이 소유자의 모든 저장소에서 액션 작업을 실행할 때 GITEA_TOKEN을 사용하여 (읽기 전용으로) 액세스할 수 있도록 허용합니다.", "actions.general.cross_repo_selected": "선택된 리포지토리", "actions.general.cross_repo_target_repos": "대상 리포지토리", - "actions.general.cross_repo_add": "대상 리포지토리 추가" + "actions.general.cross_repo_add": "대상 리포지토리 추가", + "settings.oauth_applications_disabled": "m8sh에서 OAuth 애플리케이션이 비활성화되었습니다. 개인 액세스 토큰을 사용하세요." } \ No newline at end of file diff --git a/options/locale/locale_lv-LV.json b/options/locale/locale_lv-LV.json index 8b725e5afb..2dc0468236 100644 --- a/options/locale/locale_lv-LV.json +++ b/options/locale/locale_lv-LV.json @@ -3057,5 +3057,6 @@ "gpg.signup.paste_key": "Ielīmējiet savu GPG publisko atslēgu", "gpg.signup.proceed": "Turpināt", "gpg.signup.submit": "Verificēt un izveidot kontu", - "gpg.signup.already_have_account": "Jums jau ir konts?" + "gpg.signup.already_have_account": "Jums jau ir konts?", + "settings.oauth_applications_disabled": "OAuth lietotnes ir atspējotas m8sh, izmantojiet personīgo piekļuves tokenu." } \ No newline at end of file diff --git a/options/locale/locale_nl-NL.json b/options/locale/locale_nl-NL.json index b3aaf71ec5..8683bc5a84 100644 --- a/options/locale/locale_nl-NL.json +++ b/options/locale/locale_nl-NL.json @@ -2108,5 +2108,6 @@ "gpg.signup.paste_key": "Plak je GPG publieke sleutel", "gpg.signup.proceed": "Doorgaan", "gpg.signup.submit": "Verifiëren & account aanmaken", - "gpg.signup.already_have_account": "Heb je al een account?" + "gpg.signup.already_have_account": "Heb je al een account?", + "settings.oauth_applications_disabled": "OAuth-applicaties zijn uitgeschakeld in m8sh, gebruik een persoonlijk toegangstoken." } \ No newline at end of file diff --git a/options/locale/locale_pl-PL.json b/options/locale/locale_pl-PL.json index c1f76f31e5..54a924c36b 100644 --- a/options/locale/locale_pl-PL.json +++ b/options/locale/locale_pl-PL.json @@ -2125,5 +2125,6 @@ "gpg.signup.paste_key": "Wklej swój publiczny klucz GPG", "gpg.signup.proceed": "Kontynuuj", "gpg.signup.submit": "Zweryfikuj i utwórz konto", - "gpg.signup.already_have_account": "Masz już konto?" + "gpg.signup.already_have_account": "Masz już konto?", + "settings.oauth_applications_disabled": "Aplikacje OAuth są wyłączone w m8sh, użyj osobistego tokena dostępu." } \ No newline at end of file diff --git a/options/locale/locale_pt-BR.json b/options/locale/locale_pt-BR.json index e1fd6ea119..0080b77a75 100644 --- a/options/locale/locale_pt-BR.json +++ b/options/locale/locale_pt-BR.json @@ -3301,5 +3301,6 @@ "gpg.signup.paste_key": "Cole sua chave pública GPG", "gpg.signup.proceed": "Continuar", "gpg.signup.submit": "Verificar e criar conta", - "gpg.signup.already_have_account": "Já tem uma conta?" + "gpg.signup.already_have_account": "Já tem uma conta?", + "settings.oauth_applications_disabled": "Aplicativos OAuth estão desabilitados no m8sh, use um token de acesso pessoal." } \ No newline at end of file diff --git a/options/locale/locale_pt-PT.json b/options/locale/locale_pt-PT.json index 73c70cfb3d..0c7494528d 100644 --- a/options/locale/locale_pt-PT.json +++ b/options/locale/locale_pt-PT.json @@ -3739,5 +3739,6 @@ "gpg.signup.paste_key": "Cole a sua chave pública GPG", "gpg.signup.proceed": "Continuar", "gpg.signup.submit": "Verificar e criar conta", - "gpg.signup.already_have_account": "Já tem uma conta?" + "gpg.signup.already_have_account": "Já tem uma conta?", + "settings.oauth_applications_disabled": "As aplicações OAuth estão desativadas em m8sh, utilize um token de acesso pessoal." } \ No newline at end of file diff --git a/options/locale/locale_ru-RU.json b/options/locale/locale_ru-RU.json index 060c4dedc9..2d8dc249ca 100644 --- a/options/locale/locale_ru-RU.json +++ b/options/locale/locale_ru-RU.json @@ -309,7 +309,7 @@ "auth.invalid_password": "Ваш пароль не совпадает с паролем, который был использован для создания учётной записи.", "auth.gpg_invalid_token_signature": "Неверная GPG-подпись или nonce", "auth.invalid_gpg_identity": "Не удалось извлечь идентификатор из GPG-ключа", - "auth.invalid_gpg_key_email": "Не удалось извлечь адрес электронной почты из GPG-ключа", + "auth.invalid_gpg_key_email": "Не удалось извлечь адрес электронной почты из GPG-ключа", "auth.reset_password_helper": "Восстановить аккаунт", "auth.reset_password_wrong_user": "Вы вошли как %s, но ссылка для восстановления учётной записи предназначена для %s", "auth.password_too_short": "Пароль не может быть короче %d символов.", @@ -2997,5 +2997,6 @@ "git.filemode.normal_file": "Обычный файл", "git.filemode.executable_file": "Исполняемый файл", "git.filemode.symbolic_link": "Символическая ссылка", - "git.filemode.submodule": "Подмодуль" + "git.filemode.submodule": "Подмодуль", + "settings.oauth_applications_disabled": "Приложения OAuth отключены в m8sh, используйте персональный токен доступа." } \ No newline at end of file diff --git a/options/locale/locale_si-LK.json b/options/locale/locale_si-LK.json index b30e1383b0..f0fe0be7ef 100644 --- a/options/locale/locale_si-LK.json +++ b/options/locale/locale_si-LK.json @@ -2204,5 +2204,6 @@ "gpg.signup.paste_key": "ඔබේ GPG පොදු යතුර අලවන්න", "gpg.signup.proceed": "ඉදිරියට යන්න", "gpg.signup.submit": "තහවුරු කර ගිණුම සාදන්න", - "gpg.signup.already_have_account": "දැනටමත් ගිණුමක් තිබේද?" + "gpg.signup.already_have_account": "දැනටමත් ගිණුමක් තිබේද?", + "settings.oauth_applications_disabled": "m8sh හි OAuth යෙදුම් අක්‍රිය කර ඇත, පෞද්ගලික ප්‍රවේශ ටෝකනය භාවිතා කරන්න." } \ No newline at end of file diff --git a/options/locale/locale_sk-SK.json b/options/locale/locale_sk-SK.json index 05d6d21062..82c8260c73 100644 --- a/options/locale/locale_sk-SK.json +++ b/options/locale/locale_sk-SK.json @@ -1175,5 +1175,6 @@ "gpg.signup.paste_key": "Vložte váš verejný GPG kľúč", "gpg.signup.proceed": "Pokračovať", "gpg.signup.submit": "Overiť a vytvoriť účet", - "gpg.signup.already_have_account": "Už máte účet?" + "gpg.signup.already_have_account": "Už máte účet?", + "settings.oauth_applications_disabled": "OAuth aplikácie sú v m8sh zakázané, použite osobný prístupový token." } \ No newline at end of file diff --git a/options/locale/locale_sv-SE.json b/options/locale/locale_sv-SE.json index 9edada0147..089e40f260 100644 --- a/options/locale/locale_sv-SE.json +++ b/options/locale/locale_sv-SE.json @@ -1748,5 +1748,6 @@ "gpg.signup.paste_key": "Klistra in din GPG publika nyckel", "gpg.signup.proceed": "Fortsätt", "gpg.signup.submit": "Verifiera och skapa konto", - "gpg.signup.already_have_account": "Har du redan ett konto?" + "gpg.signup.already_have_account": "Har du redan ett konto?", + "settings.oauth_applications_disabled": "OAuth-applikationer är inaktiverade i m8sh, använd personlig åtkomsttoken." } \ No newline at end of file diff --git a/options/locale/locale_tr-TR.json b/options/locale/locale_tr-TR.json index 7d15ce1995..44a336c6e8 100644 --- a/options/locale/locale_tr-TR.json +++ b/options/locale/locale_tr-TR.json @@ -3758,5 +3758,6 @@ "gpg.signup.paste_key": "GPG açık anahtarınızı yapıştırın", "gpg.signup.proceed": "Devam et", "gpg.signup.submit": "Doğrula ve hesap oluştur", - "gpg.signup.already_have_account": "Zaten hesabınız var mı?" + "gpg.signup.already_have_account": "Zaten hesabınız var mı?", + "settings.oauth_applications_disabled": "m8sh içinde OAuth uygulamaları devre dışı bırakıldı, kişisel erişim tokeni kullanın." } \ No newline at end of file diff --git a/options/locale/locale_uk-UA.json b/options/locale/locale_uk-UA.json index 346b998748..d0ccba61f5 100644 --- a/options/locale/locale_uk-UA.json +++ b/options/locale/locale_uk-UA.json @@ -3197,5 +3197,6 @@ "gpg.signup.paste_key": "Вставте ваш публічний GPG ключ", "gpg.signup.proceed": "Продовжити", "gpg.signup.submit": "Підтвердити та створити акаунт", - "gpg.signup.already_have_account": "Вже маєте акаунт?" + "gpg.signup.already_have_account": "Вже маєте акаунт?", + "settings.oauth_applications_disabled": "Програми OAuth вимкнено в m8sh, використовуйте персональний токен доступу." } \ No newline at end of file diff --git a/options/locale/locale_zh-CN.json b/options/locale/locale_zh-CN.json index 234d18b02e..2e0b49861d 100644 --- a/options/locale/locale_zh-CN.json +++ b/options/locale/locale_zh-CN.json @@ -3902,5 +3902,6 @@ "actions.general.cross_repo_desc": "允许此所有者下的所有仓库在运行工作流任务时,通过 GITEA_TOKEN 以只读方式访问所选仓库。", "actions.general.cross_repo_selected": "选择的仓库", "actions.general.cross_repo_target_repos": "目标仓库", - "actions.general.cross_repo_add": "添加目标仓库" + "actions.general.cross_repo_add": "添加目标仓库", + "settings.oauth_applications_disabled": "m8sh 中已禁用 OAuth 应用,请使用个人访问令牌。" } \ No newline at end of file diff --git a/options/locale/locale_zh-TW.json b/options/locale/locale_zh-TW.json index cba37d1d76..abe55d138f 100644 --- a/options/locale/locale_zh-TW.json +++ b/options/locale/locale_zh-TW.json @@ -3312,5 +3312,6 @@ "gpg.signup.paste_key": "貼上您的 GPG 公開金鑰", "gpg.signup.proceed": "繼續", "gpg.signup.submit": "驗證並建立帳戶", - "gpg.signup.already_have_account": "已有帳戶?" + "gpg.signup.already_have_account": "已有帳戶?", + "settings.oauth_applications_disabled": "m8sh 中已停用 OAuth 應用程式,請使用個人存取權杖。" } \ No newline at end of file diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index ba47dcab15..68fe488a6c 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -4,7 +4,9 @@ package user import ( + "fmt" "net/http" + "strings" asymkey_model "gitea.dev/models/asymkey" "gitea.dev/models/db" @@ -136,12 +138,25 @@ func CreateGPGKey(ctx *context.APIContext) { // DeleteGPGKey remove a GPG key belonging to the authenticated user func DeleteGPGKey(ctx *context.APIContext) { - if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { - ctx.APIErrorNotFound("gpg keys setting is not allowed to be changed") + keyID := ctx.PathParamInt64("id") + + key, err := asymkey_model.GetGPGKeyForUserByID(ctx, ctx.Doer.ID, keyID) + if err != nil { + if asymkey_model.IsErrGPGKeyNotExist(err) { + ctx.APIErrorNotFound() + } else { + ctx.APIErrorInternal(err) + } return } - if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.PathParamInt64("id")); err != nil { + // m8sh: Protect last key for primary registration email + if err := checkLastPrimaryEmailKey(ctx, ctx.Doer, key); err != nil { + ctx.APIError(http.StatusForbidden, err.Error()) + return + } + + if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, keyID); err != nil { ctx.APIErrorInternal(err) return } @@ -149,6 +164,56 @@ func DeleteGPGKey(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } +// checkLastPrimaryEmailKey returns error if deleting this key would remove +// the last remaining key (including its subkeys) that contains the primary email. +func checkLastPrimaryEmailKey(ctx *context.APIContext, user *user_model.User, deletingKey *asymkey_model.GPGKey) error { + primaryEmail := user.Email + + if !keyContainsEmail(deletingKey, primaryEmail) { + return nil // safe to delete + } + + allKeys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{ + OwnerID: user.ID, + IncludeSubKeys: true, + }) + if err != nil { + return fmt.Errorf("failed to load keys") + } + + count := 0 + for _, k := range allKeys { + if keyContainsEmail(k, primaryEmail) { + count++ + } + } + + if count <= 1 { + return fmt.Errorf("cannot delete the last GPG key for your primary email (%s)", primaryEmail) + } + + return nil +} + +// keyContainsEmail checks if a GPG key (or any of its subkeys) contains the given email +func keyContainsEmail(key *asymkey_model.GPGKey, email string) bool { + for _, e := range key.Emails { + if strings.EqualFold(e.Email, email) { + return true + } + } + + for _, sub := range key.SubsKey { + for _, e := range sub.Emails { + if strings.EqualFold(e.Email, email) { + return true + } + } + } + + return false +} + // HandleAddGPGKeyError handle add GPGKey error func HandleAddGPGKeyError(ctx *context.APIContext, err error, nonce string) { switch { diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 81d76852fb..ac019e0706 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -963,6 +963,27 @@ func SignInGPGPost(ctx *context.Context) { user, err := asymkey_model.VerifyGPGSignature(ctx, form.Nonce, form.Signature) if err != nil { ctx.Data["Err_GPGSign"] = true + ctx.Data["Flash"] = &middleware.Flash{ + ErrorMsg: ctx.Locale.TrString("auth.gpg_invalid_token_signature"), + } + ctx.HTML(http.StatusOK, tplSignIn) + return + } + + if !user.IsActive { + ctx.Data["Err_GPGSign"] = true + ctx.Data["Flash"] = &middleware.Flash{ + ErrorMsg: ctx.Locale.TrString("auth.active_your_account"), + } + ctx.HTML(http.StatusOK, tplSignIn) + return + } + + if user.ProhibitLogin { + ctx.Data["Err_GPGSign"] = true + ctx.Data["Flash"] = &middleware.Flash{ + ErrorMsg: ctx.Locale.TrString("auth.prohibit_login"), + } ctx.HTML(http.StatusOK, tplSignIn) return } diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go index 0ed20329b4..55306508f2 100644 --- a/routers/web/auth/oauth2_provider.go +++ b/routers/web/auth/oauth2_provider.go @@ -182,7 +182,8 @@ func AuthorizeOAuth(ctx *context.Context) { if len(errs) > 0 { var errstring strings.Builder for _, e := range errs { - errstring.WriteString(e.Error() + "\n") + errstring.WriteString(e.Error()) + errstring.WriteString("\n") } ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring.String())) return diff --git a/services/user/user.go b/services/user/user.go index 91551fe2be..1c1e0fa8a2 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -5,9 +5,8 @@ package user import ( "context" + "errors" "fmt" - "os" - "strings" "time" "gitea.dev/models/db" @@ -22,98 +21,15 @@ import ( "gitea.dev/modules/storage" "gitea.dev/modules/structs" "gitea.dev/modules/util" - "gitea.dev/services/agit" asymkey_service "gitea.dev/services/asymkey" org_service "gitea.dev/services/org" "gitea.dev/services/packages" - container_service "gitea.dev/services/packages/container" repo_service "gitea.dev/services/repository" ) // RenameUser renames a user func RenameUser(ctx context.Context, u *user_model.User, newUserName string, doer *user_model.User) error { - if newUserName == u.Name { - return nil - } - - // Non-local users are not allowed to change their own username, but admins are - isExternalUser := !u.IsOrganization() && !u.IsLocal() - if isExternalUser && !doer.IsAdmin { - return user_model.ErrUserIsNotLocal{UID: u.ID, Name: u.Name} - } - - if err := user_model.IsUsableUsername(newUserName); err != nil { - return err - } - - onlyCapitalization := strings.EqualFold(newUserName, u.Name) - oldUserName := u.Name - - if onlyCapitalization { - u.Name = newUserName - if err := user_model.UpdateUserCols(ctx, u, "name"); err != nil { - u.Name = oldUserName - return err - } - return repo_model.UpdateRepositoryOwnerNames(ctx, u.ID, newUserName) - } - - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - isExist, err := user_model.IsUserExist(ctx, u.ID, newUserName) - if err != nil { - return err - } - if isExist { - return user_model.ErrUserAlreadyExist{ - Name: newUserName, - } - } - - if err = repo_model.UpdateRepositoryOwnerName(ctx, oldUserName, newUserName); err != nil { - return err - } - - if err = user_model.NewUserRedirect(ctx, u.ID, oldUserName, newUserName); err != nil { - return err - } - - if err := agit.UserNameChanged(ctx, u, newUserName); err != nil { - return err - } - if err := container_service.UpdateRepositoryNames(ctx, u, newUserName); err != nil { - return err - } - - u.Name = newUserName - u.LowerName = strings.ToLower(newUserName) - if err := user_model.UpdateUserCols(ctx, u, "name", "lower_name"); err != nil { - u.Name = oldUserName - u.LowerName = strings.ToLower(oldUserName) - return err - } - - // Do not fail if directory does not exist - if err = util.Rename(user_model.UserPath(oldUserName), user_model.UserPath(newUserName)); err != nil && !os.IsNotExist(err) { - u.Name = oldUserName - u.LowerName = strings.ToLower(oldUserName) - return fmt.Errorf("rename user directory: %w", err) - } - - if err = committer.Commit(); err != nil { - u.Name = oldUserName - u.LowerName = strings.ToLower(oldUserName) - if err2 := util.Rename(user_model.UserPath(newUserName), user_model.UserPath(oldUserName)); err2 != nil && !os.IsNotExist(err2) { - log.Error("Unable to rollback directory change during failed username change from: %s to: %s. DB Error: %v. Filesystem Error: %v", oldUserName, newUserName, err, err2) - return fmt.Errorf("failed to rollback directory change during failed username change from: %s to: %s. DB Error: %w. Filesystem Error: %v", oldUserName, newUserName, err, err2) - } - return err - } - return nil + return errors.New("username change is disabled in m8sh") } // DeleteUser completely and permanently deletes everything of a user, diff --git a/templates/org/settings/applications.tmpl b/templates/org/settings/applications.tmpl index eb12d3fa2a..f88e085ab0 100644 --- a/templates/org/settings/applications.tmpl +++ b/templates/org/settings/applications.tmpl @@ -1,9 +1,12 @@ {{template "org/settings/layout_head" (dict "pageClass" "organization settings options")}} -
-

- {{ctx.Locale.Tr "settings.applications"}} -

- - {{template "user/settings/applications_oauth2_list" .}} -
-{{template "org/settings/layout_footer" .}} +
+

+ {{ctx.Locale.Tr "settings.applications"}} +

+
+
+

{{ctx.Locale.Tr "settings.oauth_applications_disabled"}}

+
+
+
+{{template "org/settings/layout_footer" .}} \ No newline at end of file diff --git a/templates/user/settings/applications_oauth2.tmpl b/templates/user/settings/applications_oauth2.tmpl index 866a1f87d3..553aa1f651 100644 --- a/templates/user/settings/applications_oauth2.tmpl +++ b/templates/user/settings/applications_oauth2.tmpl @@ -1,6 +1 @@ -

- {{ctx.Locale.Tr "settings.manage_oauth2_applications"}} -

- -{{template "user/settings/applications_oauth2_list" .}} - +{{/* OAuth disabled */}} \ No newline at end of file diff --git a/templates/user/settings/applications_oauth2_edit_form.tmpl b/templates/user/settings/applications_oauth2_edit_form.tmpl index 4323e4d985..553aa1f651 100644 --- a/templates/user/settings/applications_oauth2_edit_form.tmpl +++ b/templates/user/settings/applications_oauth2_edit_form.tmpl @@ -1,57 +1 @@ -

- {{ctx.Locale.Tr "settings.edit_oauth2_application"}} -

-
-

{{ctx.Locale.Tr "settings.oauth2_application_create_description"}}

-
-
-
- - -
- {{if .ClientSecret}} -
- - -
- {{else}} -
- - -
- {{end}} -
- -
- {{ctx.Locale.Tr "settings.oauth2_regenerate_secret_hint"}} - -
-
-
-
-
-
- - -
-
- - -
-
-
- - -
-
-
-
- - -
-
- -
-
+{{/* OAuth disabled */}} \ No newline at end of file diff --git a/templates/user/settings/applications_oauth2_list.tmpl b/templates/user/settings/applications_oauth2_list.tmpl index cbec1de7b7..c17c0757a0 100644 --- a/templates/user/settings/applications_oauth2_list.tmpl +++ b/templates/user/settings/applications_oauth2_list.tmpl @@ -1,79 +1 @@ -
-
-
- {{ctx.Locale.Tr "settings.oauth2_application_create_description"}} -
- {{range .Applications}} -
-
- {{svg "octicon-apps" 32}} -
-
-
{{.Name}}
-
- {{ctx.Locale.Tr "settings.oauth2_client_id"}} - {{.ClientID}} -
-
- {{$isBuiltin := and $.BuiltinApplications (index $.BuiltinApplications .ClientID)}} -
- {{if $isBuiltin}} - {{ctx.Locale.Tr "locked"}} - {{else}} - - {{svg "octicon-pencil"}} - {{ctx.Locale.Tr "settings.oauth2_application_edit"}} - - - {{end}} -
-
- {{end}} -
- - -
- -
-
-

{{ctx.Locale.Tr "settings.create_oauth2_application"}}

-
-
- - -
-
- - -
-
-
- - -
-
-
-
- - -
-
- -
-
-
+{{/* OAuth2 applications completely disabled in m8sh */}} \ No newline at end of file diff --git a/templates/user/settings/grants_oauth2.tmpl b/templates/user/settings/grants_oauth2.tmpl index ed2cfad038..4283018462 100644 --- a/templates/user/settings/grants_oauth2.tmpl +++ b/templates/user/settings/grants_oauth2.tmpl @@ -1,40 +1 @@ -

- {{ctx.Locale.Tr "settings.authorized_oauth2_applications"}} -

-
-
-
- {{ctx.Locale.Tr "settings.authorized_oauth2_applications_description"}} -
- {{range .Grants}} -
-
- {{svg "octicon-key" 32}} -
-
-
{{.Application.Name}}
-
- {{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}} -
-
-
- -
-
- {{end}} -
- - -
+{{/* OAuth2 grants completely disabled in m8sh (GPG auth only) */}} \ No newline at end of file diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl index 9530c2d366..4fb2630f7a 100644 --- a/templates/user/settings/navbar.tmpl +++ b/templates/user/settings/navbar.tmpl @@ -5,9 +5,9 @@ {{ctx.Locale.Tr "settings.profile"}} {{if not ($.UserDisabledFeatures.Contains "manage_credentials" "deletion")}} - + {{end}} {{if $.EnableNotifyMail}} diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index bd39e0a1b4..c76cf90aec 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -6,22 +6,13 @@

{{ctx.Locale.Tr "settings.profile_desc"}}

-
- - - {{if or (not .SignedUser.IsLocal) ($.UserDisabledFeatures.Contains "change_username") .IsReverseProxy}} -

{{ctx.Locale.Tr "settings.password_username_disabled"}}

- {{end}} +
+ +
- - {{if ($.UserDisabledFeatures.Contains "change_full_name")}} -

{{ctx.Locale.Tr "settings.password_full_name_disabled"}}

- {{end}} +
diff --git a/web_src/js/features/gpg-nonce.ts b/web_src/js/features/gpg-nonce.ts index f769a409da..2bc76b7382 100644 --- a/web_src/js/features/gpg-nonce.ts +++ b/web_src/js/features/gpg-nonce.ts @@ -1,58 +1,16 @@ -export function generateNonce(): string { - const existing = sessionStorage.getItem('gpg_signup_nonce'); - if (existing) return existing; - +export function makeNonce(): string { const ts = Math.floor(Date.now() / 1000).toString(16).padStart(8, '0'); const arr = new Uint8Array(28); crypto.getRandomValues(arr); - const random = Array.from(arr, (b) => b.toString(16).padStart(2, '0')).join(''); - const nonce = ts + random; - sessionStorage.setItem('gpg_signup_nonce', nonce); - return nonce; + return ts + Array.from(arr, (b) => b.toString(16).padStart(2, '0')).join(''); } export function buildSignCommand(nonce: string, keyID?: string): string { - if (keyID) { - return `echo "${nonce}" | gpg -a --default-key ${keyID} --detach-sig`; - } + if (keyID) return `echo "${nonce}" | gpg -a --default-key ${keyID} --detach-sig`; return `echo "${nonce}" | gpg -a --detach-sig`; } export function validateSignature(sig: string): boolean { return sig.startsWith('-----BEGIN PGP SIGNED MESSAGE-----') || - sig.startsWith('-----BEGIN PGP SIGNATURE-----'); -} - -export function initGpgNonceWidget(opts: { - tokenFieldId: string; - nonceInputId: string; - signCommandId: string; - copyBtnId: string; - signatureId: string; - formId: string; - keyID?: string; -}): string | null { - const tokenField = document.querySelector(opts.tokenFieldId); - if (!tokenField) return null; - - const nonce = generateNonce(); - const cmd = buildSignCommand(nonce, opts.keyID); - - (tokenField as HTMLInputElement).value = nonce; - (document.querySelector(opts.nonceInputId) as HTMLInputElement).value = nonce; - (document.querySelector(opts.signCommandId) as HTMLInputElement).value = cmd; - - document.querySelector(opts.copyBtnId)?.addEventListener('click', () => { - navigator.clipboard.writeText(cmd); - }); - - document.querySelector(opts.formId)?.addEventListener('submit', (e) => { - const sig = (document.querySelector(opts.signatureId) as HTMLTextAreaElement).value.trim(); - if (!validateSignature(sig)) { - e.preventDefault(); - alert('Paste the GPG signed output.'); - } - }); - - return nonce; + sig.startsWith('-----BEGIN PGP SIGNATURE-----'); } diff --git a/web_src/js/features/user-auth-gpg-signin.ts b/web_src/js/features/user-auth-gpg-signin.ts index a6ab71a581..dcd676e434 100644 --- a/web_src/js/features/user-auth-gpg-signin.ts +++ b/web_src/js/features/user-auth-gpg-signin.ts @@ -1,30 +1,27 @@ -import { buildSignCommand, validateSignature } from './gpg-nonce.ts'; +import {makeNonce, buildSignCommand, validateSignature} from './gpg-nonce.ts'; export function initGpgSignin() { - const tokenField = document.getElementById('token-field') as HTMLInputElement; + const tokenField = document.querySelector('token-field') as HTMLInputElement; if (!tokenField) return; - const ts = Math.floor(Date.now() / 1000).toString(16).padStart(8, '0'); - const arr = new Uint8Array(28); - crypto.getRandomValues(arr); - const random = Array.from(arr, (b) => b.toString(16).padStart(2, '0')).join(''); - const nonce = ts + random; - + const nonce = makeNonce(); const cmd = buildSignCommand(nonce); tokenField.value = nonce; - (document.getElementById('hidden-nonce') as HTMLInputElement).value = nonce; - (document.getElementById('sign-command') as HTMLInputElement).value = cmd; + (document.querySelector('hidden-nonce') as HTMLInputElement).value = nonce; + (document.querySelector('sign-command') as HTMLInputElement).value = cmd; - document.getElementById('btn-copy-cmd')?.addEventListener('click', () => { - navigator.clipboard.writeText(cmd); + document.querySelector('btn-copy-cmd')?.addEventListener('click', () => { + navigator.clipboard.writeText( + (document.querySelector('sign-command') as HTMLInputElement).value, + ); }); - document.getElementById('signin-form')?.addEventListener('submit', (e) => { - const sig = (document.getElementById('gpg_signature') as HTMLTextAreaElement).value.trim(); + document.querySelector('signin-form')?.addEventListener('submit', (e) => { + const sig = (document.querySelector('gpg_signature') as HTMLTextAreaElement).value.trim(); if (!validateSignature(sig)) { e.preventDefault(); alert('Paste the GPG signed output.'); } }); -} \ No newline at end of file +} diff --git a/web_src/js/features/user-auth-gpg-signup.ts b/web_src/js/features/user-auth-gpg-signup.ts index 7c7d5ef33b..28b96ec4df 100644 --- a/web_src/js/features/user-auth-gpg-signup.ts +++ b/web_src/js/features/user-auth-gpg-signup.ts @@ -1,47 +1,49 @@ -import { generateNonce, buildSignCommand, validateSignature } from './gpg-nonce.ts'; +import {makeNonce, buildSignCommand, validateSignature} from './gpg-nonce.ts'; export function initGpgSignup() { - const btnProceed = document.querySelector('#btn-proceed'); + const btnProceed = document.querySelector('btn-proceed'); if (!btnProceed) return; - const nonce = generateNonce(); + const nonce = makeNonce(); + + // if step-sign already visible (error re-render), populate now + const stepSign = document.querySelector('step-sign'); + if (stepSign && (stepSign as HTMLElement).style.display !== 'none') { + (document.querySelector('token-field') as HTMLInputElement).value = nonce; + (document.querySelector('hidden-nonce') as HTMLInputElement).value = nonce; + (document.querySelector('sign-command') as HTMLInputElement).value = buildSignCommand(nonce); + } btnProceed.addEventListener('click', () => { - const key = (document.querySelector('#gpg_key') as HTMLTextAreaElement).value.trim(); + const key = (document.querySelector('gpg_key') as HTMLTextAreaElement).value + .trim() + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n'); + if (!key || !key.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')) { alert('Paste a valid armored GPG public key.'); return; } - (document.querySelector('#token-field') as HTMLInputElement).value = nonce; - (document.querySelector('#hidden-nonce') as HTMLInputElement).value = nonce; - (document.querySelector('#hidden-gpg-key') as HTMLInputElement).value = key; - (document.querySelector('#sign-command') as HTMLInputElement).value = - buildSignCommand(nonce); - (document.querySelector('#step-key') as HTMLElement).style.display = 'none'; - (document.querySelector('#step-sign') as HTMLElement).style.display = 'block'; + + (document.querySelector('token-field') as HTMLInputElement).value = nonce; + (document.querySelector('hidden-nonce') as HTMLInputElement).value = nonce; + (document.querySelector('hidden-gpg-key') as HTMLInputElement).value = key; + (document.querySelector('sign-command') as HTMLInputElement).value = buildSignCommand(nonce); + (document.querySelector('step-key') as HTMLElement).style.display = 'none'; + (document.querySelector('step-sign') as HTMLElement).style.display = 'block'; }); - document.querySelector('#signup-form')?.addEventListener('submit', (e) => { - const sig = (document.querySelector('#gpg_signature') as HTMLTextAreaElement).value.trim(); - if (!validateSignature(sig)) { - e.preventDefault(); - alert('Paste the GPG signed output.'); - } - }); - - document.querySelector('#btn-copy-cmd')?.addEventListener('click', () => { + document.querySelector('btn-copy-cmd')?.addEventListener('click', () => { navigator.clipboard.writeText( - (document.querySelector('#sign-command') as HTMLInputElement).value, + (document.querySelector('sign-command') as HTMLInputElement).value, ); }); - document.querySelector('#signup-form')?.addEventListener('submit', (e) => { - const sig = (document.querySelector('#gpg_signature') as HTMLTextAreaElement).value.trim(); + document.querySelector('signup-form')?.addEventListener('submit', (e) => { + const sig = (document.querySelector('gpg_signature') as HTMLTextAreaElement).value.trim(); if (!validateSignature(sig)) { e.preventDefault(); alert('Paste the GPG signed output.'); - return; } - sessionStorage.removeItem('gpg_signup_nonce'); }); } diff --git a/web_src/js/features/user-settings-gpg-key.ts b/web_src/js/features/user-settings-gpg-key.ts index 9b7088608d..546ca830c5 100644 --- a/web_src/js/features/user-settings-gpg-key.ts +++ b/web_src/js/features/user-settings-gpg-key.ts @@ -1,27 +1,42 @@ -import { initGpgNonceWidget } from './gpg-nonce.ts'; +import {makeNonce, buildSignCommand} from './gpg-nonce.ts'; export function initGpgKeySettings() { - // add key flow — only active when Err_Signature is present - initGpgNonceWidget({ - tokenFieldId: 'add-key-token-field', - nonceInputId: 'add-key-hidden-nonce', - signCommandId: 'add-key-sign-command', - copyBtnId: 'add-key-btn-copy-cmd', - signatureId: 'gpg-key-signature', - formId: 'add-key-form', + const addTokenField = document.querySelector('add-key-token-field') as HTMLInputElement; + if (addTokenField) { + const nonce = makeNonce(); + addTokenField.value = nonce; + (document.querySelector('add-key-hidden-nonce') as HTMLInputElement).value = nonce; + (document.querySelector('add-key-sign-command') as HTMLInputElement).value = buildSignCommand(nonce); + document.querySelector('add-key-btn-copy-cmd')?.addEventListener('click', () => { + navigator.clipboard.writeText( + (document.querySelector('add-key-sign-command') as HTMLInputElement).value, + ); + }); + } + + const verifyTokenField = document.querySelector('verify-key-token-field') as HTMLInputElement; + if (verifyTokenField) { + const nonce = makeNonce(); + const keyID = (document.querySelector('verify-key-form') as HTMLElement)?.getAttribute('keyId') ?? ''; + verifyTokenField.value = nonce; + (document.querySelector('verify-key-hidden-nonce') as HTMLInputElement).value = nonce; + (document.querySelector('verify-key-sign-command') as HTMLInputElement).value = buildSignCommand(nonce, keyID); + document.querySelector('verify-key-btn-copy-cmd')?.addEventListener('click', () => { + navigator.clipboard.writeText( + (document.querySelector('verify-key-sign-command') as HTMLInputElement).value, + ); + }); + } + + document.querySelector('add-key-btn-copy-cmd')?.addEventListener('click', () => { + navigator.clipboard.writeText( + (document.querySelector('add-key-sign-command') as HTMLInputElement).value, + ); }); - // verify flow — keyID known from data attribute - const verifyForm = document.querySelector('#verify-key-form'); - const keyID = verifyForm?.getAttribute('keyId') ?? ''; - - initGpgNonceWidget({ - tokenFieldId: 'verify-key-token-field', - nonceInputId: 'verify-key-hidden-nonce', - signCommandId: 'verify-key-sign-command', - copyBtnId: 'verify-key-btn-copy-cmd', - signatureId: 'gpg-key-signature', - formId: 'verify-key-form', - keyID, + document.querySelector('verify-key-btn-copy-cmd')?.addEventListener('click', () => { + navigator.clipboard.writeText( + (document.querySelector('verify-key-sign-command') as HTMLInputElement).value, + ); }); }