Gpg key more fixes #6

Merged
d merged 5 commits from gpg-key-more-fixes into main 2026-06-13 16:54:01 +00:00
43 changed files with 250 additions and 431 deletions
+2 -1
View File
@@ -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 <code>m8sh</code> zakázány, použijte osobní přístupový token."
}
+2 -1
View File
@@ -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 <code>m8sh</code> deaktiviert, verwenden Sie einen persönlichen Zugriffstoken."
}
+2 -1
View File
@@ -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 είναι απενεργοποιημένες στο <code>m8sh</code>, χρησιμοποιήστε προσωπικό διακριτικό πρόσβασης."
}
+2 -1
View File
@@ -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 <code>m8sh</code>, use personal access token."
}
+2 -1
View File
@@ -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 <code>m8sh</code>, usa un token de acceso personal."
}
+2 -1
View File
@@ -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 در <code>m8sh</code> غیرفعال هستند، از توکن دسترسی شخصی استفاده کنید."
}
+2 -1
View File
@@ -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ä <code>m8sh</code>-järjestelmässä, käytä henkilökohtaista käyttöoikeustunnusta."
}
+2 -1
View File
@@ -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 à laide de GITEA_TOKEN lors de lexécution des tâches dactions.",
"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 <code>m8sh</code>, utilisez un jeton d'accès personnel."
}
+2 -1
View File
@@ -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 <code>m8sh</code>, bain úsáid as comhartha rochtana pearsanta."
}
+2 -1
View File
@@ -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 <code>m8sh</code>-ban, használjon személyes hozzáférési tokent."
}
+2 -1
View File
@@ -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 <code>m8sh</code>, gunakan token akses pribadi."
}
+2 -1
View File
@@ -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 í <code>m8sh</code>, notaðu persónulegan aðgangstákn."
}
+2 -1
View File
@@ -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 <code>m8sh</code>, usa un token di accesso personale."
}
+2 -1
View File
@@ -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": "<code>m8sh</code> では OAuth アプリケーションは無効です。個人用アクセストークンを使用してください。"
}
+2 -1
View File
@@ -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": "<code>m8sh</code>에서 OAuth 애플리케이션이 비활성화되었습니다. 개인 액세스 토큰을 사용하세요."
}
+2 -1
View File
@@ -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 <code>m8sh</code>, izmantojiet personīgo piekļuves tokenu."
}
+2 -1
View File
@@ -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 <code>m8sh</code>, gebruik een persoonlijk toegangstoken."
}
+2 -1
View File
@@ -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 <code>m8sh</code>, użyj osobistego tokena dostępu."
}
+2 -1
View File
@@ -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 <code>m8sh</code>, use um token de acesso pessoal."
}
+2 -1
View File
@@ -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 <code>m8sh</code>, utilize um token de acesso pessoal."
}
+3 -2
View File
@@ -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 отключены в <code>m8sh</code>, используйте персональный токен доступа."
}
+2 -1
View File
@@ -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": "<code>m8sh</code> හි OAuth යෙදුම් අක්‍රිය කර ඇත, පෞද්ගලික ප්‍රවේශ ටෝකනය භාවිතා කරන්න."
}
+2 -1
View File
@@ -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 <code>m8sh</code> zakázané, použite osobný prístupový token."
}
+2 -1
View File
@@ -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 <code>m8sh</code>, använd personlig åtkomsttoken."
}
+2 -1
View File
@@ -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": "<code>m8sh</code> içinde OAuth uygulamaları devre dışı bırakıldı, kişisel erişim tokeni kullanın."
}
+2 -1
View File
@@ -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 вимкнено в <code>m8sh</code>, використовуйте персональний токен доступу."
}
+2 -1
View File
@@ -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": "<code>m8sh</code> 中已禁用 OAuth 应用,请使用个人访问令牌。"
}
+2 -1
View File
@@ -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": "<code>m8sh</code> 中已停用 OAuth 應用程式,請使用個人存取權杖。"
}
+68 -3
View File
@@ -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 {
+21
View File
@@ -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
}
+2 -1
View File
@@ -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
+2 -86
View File
@@ -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,
+11 -8
View File
@@ -1,9 +1,12 @@
{{template "org/settings/layout_head" (dict "pageClass" "organization settings options")}}
<div class="org-setting-content">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.applications"}}
</h4>
{{template "user/settings/applications_oauth2_list" .}}
</div>
{{template "org/settings/layout_footer" .}}
<div class="org-setting-content">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.applications"}}
</h4>
<div class="ui attached segment">
<div class="ui info">
<p>{{ctx.Locale.Tr "settings.oauth_applications_disabled"}}</p>
</div>
</div>
</div>
{{template "org/settings/layout_footer" .}}
@@ -1,6 +1 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.manage_oauth2_applications"}}
</h4>
{{template "user/settings/applications_oauth2_list" .}}
{{/* OAuth disabled */}}
@@ -1,57 +1 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.edit_oauth2_application"}}
</h4>
<div class="ui attached segment">
<p>{{ctx.Locale.Tr "settings.oauth2_application_create_description"}}</p>
</div>
<div class="ui attached segment form ignore-dirty">
<div class="field">
<label for="client-id">{{ctx.Locale.Tr "settings.oauth2_client_id"}}</label>
<input id="client-id" readonly value="{{.App.ClientID}}">
</div>
{{if .ClientSecret}}
<div class="field">
<label for="client-secret">{{ctx.Locale.Tr "settings.oauth2_client_secret"}}</label>
<input id="client-secret" type="text" readonly value="{{.ClientSecret}}">
</div>
{{else}}
<div class="field">
<label for="client-secret">{{ctx.Locale.Tr "settings.oauth2_client_secret"}}</label>
<input id="client-secret" type="password" readonly value="averysecuresecret">
</div>
{{end}}
<div class="item">
<!-- TODO add regenerate secret functionality */ -->
<form class="ui form ignore-dirty" action="{{.FormActionPath}}/regenerate_secret" method="post">
{{ctx.Locale.Tr "settings.oauth2_regenerate_secret_hint"}}
<button class="ui mini button tw-ml-2" type="submit">{{ctx.Locale.Tr "settings.oauth2_regenerate_secret"}}</button>
</form>
</div>
</div>
<div class="ui bottom attached segment">
<form class="ui form ignore-dirty" action="{{.FormActionPath}}" method="post">
<div class="field {{if .Err_AppName}}error{{end}}">
<label for="application-name">{{ctx.Locale.Tr "settings.oauth2_application_name"}}</label>
<input id="application-name" value="{{.App.Name}}" name="application_name" required maxlength="255">
</div>
<div class="field {{if .Err_RedirectURI}}error{{end}}">
<label for="redirect-uris">{{ctx.Locale.Tr "settings.oauth2_redirect_uris"}}</label>
<textarea name="redirect_uris" id="redirect-uris" required>{{StringUtils.Join .App.RedirectURIs "\n"}}</textarea>
</div>
<div class="field {{if .Err_ConfidentialClient}}error{{end}}">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label>
<input class="disable-setting" type="checkbox" name="confidential_client" data-target="#skip-secondary-authorization" {{if .App.ConfidentialClient}}checked{{end}}>
</div>
</div>
<div class="field {{if .Err_SkipSecondaryAuthorization}}error{{end}} {{if .App.ConfidentialClient}}disabled{{end}}" id="skip-secondary-authorization">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "settings.oauth2_skip_secondary_authorization"}}</label>
<input type="checkbox" name="skip_secondary_authorization" {{if .App.SkipSecondaryAuthorization}}checked{{end}}>
</div>
</div>
<button class="ui primary button">
{{ctx.Locale.Tr "settings.save_application"}}
</button>
</form>
</div>
{{/* OAuth disabled */}}
@@ -1,79 +1 @@
<div class="ui attached segment">
<div class="flex-divided-list items-with-main">
<div class="item">
{{ctx.Locale.Tr "settings.oauth2_application_create_description"}}
</div>
{{range .Applications}}
<div class="item tw-items-center">
<div class="item-leading">
{{svg "octicon-apps" 32}}
</div>
<div class="item-main">
<div class="item-title">{{.Name}}</div>
<div class="item-body">
{{ctx.Locale.Tr "settings.oauth2_client_id"}}
<span class="ui label">{{.ClientID}}</span>
</div>
</div>
{{$isBuiltin := and $.BuiltinApplications (index $.BuiltinApplications .ClientID)}}
<div class="item-trailing">
{{if $isBuiltin}}
<span class="ui basic label" data-tooltip-content="{{ctx.Locale.Tr "settings.oauth2_application_locked"}}">{{ctx.Locale.Tr "locked"}}</span>
{{else}}
<a href="{{$.Link}}/oauth2/{{.ID}}" class="ui primary tiny button">
{{svg "octicon-pencil"}}
{{ctx.Locale.Tr "settings.oauth2_application_edit"}}
</a>
<button class="ui red tiny button delete-button" data-modal-id="remove-gitea-oauth2-application"
data-url="{{$.Link}}/oauth2/{{.ID}}/delete">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.delete_key"}}
</button>
{{end}}
</div>
</div>
{{end}}
</div>
<div class="ui g-modal-confirm delete modal" id="remove-gitea-oauth2-application">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.remove_oauth2_application"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "settings.oauth2_application_remove_description"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</div>
<div class="ui bottom attached segment">
<details {{if .application_name}}open{{end}}>
<summary><h4 class="ui header tw-inline-block tw-my-2">{{ctx.Locale.Tr "settings.create_oauth2_application"}}</h4></summary>
<form class="ui form ignore-dirty" action="{{.Link}}/oauth2" method="post">
<div class="field {{if .Err_AppName}}error{{end}}">
<label for="application-name">{{ctx.Locale.Tr "settings.oauth2_application_name"}}</label>
<input id="application-name" name="application_name" value="{{.application_name}}" required maxlength="255">
</div>
<div class="field {{if .Err_RedirectURI}}error{{end}}">
<label for="redirect-uris">{{ctx.Locale.Tr "settings.oauth2_redirect_uris"}}</label>
<textarea name="redirect_uris" id="redirect-uris"></textarea>
</div>
<div class="field {{if .Err_ConfidentialClient}}error{{end}}">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label>
<input class="disable-setting" type="checkbox" name="confidential_client" data-target="#skip-secondary-authorization" checked>
</div>
</div>
<div class="field {{if .Err_SkipSecondaryAuthorization}}error{{end}} disabled" id="skip-secondary-authorization">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "settings.oauth2_skip_secondary_authorization"}}</label>
<input type="checkbox" name="skip_secondary_authorization">
</div>
</div>
<button class="ui primary button">
{{ctx.Locale.Tr "settings.create_oauth2_application_button"}}
</button>
</form>
</details>
</div>
{{/* OAuth2 applications completely disabled in m8sh */}}
+1 -40
View File
@@ -1,40 +1 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.authorized_oauth2_applications"}}
</h4>
<div class="ui attached segment">
<div class="flex-divided-list items-with-main">
<div class="item">
{{ctx.Locale.Tr "settings.authorized_oauth2_applications_description"}}
</div>
{{range .Grants}}
<div class="item">
<div class="item-leading">
{{svg "octicon-key" 32}}
</div>
<div class="item-main">
<div class="item-title">{{.Application.Name}}</div>
<div class="item-body">
<i>{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}</i>
</div>
</div>
<div class="item-trailing">
<button class="ui red tiny button delete-button" data-modal-id="revoke-gitea-oauth2-grant"
data-url="{{AppSubUrl}}/user/settings/applications/oauth2/{{.ApplicationID}}/revoke/{{.ID}}">
{{ctx.Locale.Tr "settings.revoke_key"}}
</button>
</div>
</div>
{{end}}
</div>
<div class="ui g-modal-confirm delete modal" id="revoke-gitea-oauth2-grant">
<div class="header">
{{svg "octicon-shield" 16 "tw-mr-1"}}
{{ctx.Locale.Tr "settings.revoke_oauth2_grant"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "settings.revoke_oauth2_grant_description"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</div>
{{/* OAuth2 grants completely disabled in m8sh (GPG auth only) */}}
+2 -2
View File
@@ -5,9 +5,9 @@
{{ctx.Locale.Tr "settings.profile"}}
</a>
{{if not ($.UserDisabledFeatures.Contains "manage_credentials" "deletion")}}
<a class="{{if .PageIsSettingsAccount}}active {{end}}item" href="{{AppSubUrl}}/user/settings/account">
<!-- <a class="{{if .PageIsSettingsAccount}}active {{end}}item" href="{{AppSubUrl}}/user/settings/account">
{{ctx.Locale.Tr "settings.account"}}
</a>
</a> m8sh doesn't support email changes/other account related actions except deletion -->
{{end}}
{{if $.EnableNotifyMail}}
<a class="{{if .PageIsSettingsNotifications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/notifications">
+4 -13
View File
@@ -6,22 +6,13 @@
<div class="ui attached segment">
<p>{{ctx.Locale.Tr "settings.profile_desc"}}</p>
<form class="ui form" action="{{.Link}}" method="post">
<div class="required field {{if .Err_Name}}error{{end}}">
<label for="username">{{ctx.Locale.Tr "username"}}
<span class="tw-text-red tw-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span>
<span class="tw-text-red tw-hidden" id="name-change-redirect-prompt"> {{ctx.Locale.Tr "settings.change_username_redirect_prompt"}}</span>
</label>
<input id="username" name="name" value="{{.SignedUser.Name}}" data-name="{{.SignedUser.Name}}" required {{if or (not .SignedUser.IsLocal) ($.UserDisabledFeatures.Contains "change_username") .IsReverseProxy}}disabled{{end}} maxlength="40">
{{if or (not .SignedUser.IsLocal) ($.UserDisabledFeatures.Contains "change_username") .IsReverseProxy}}
<p class="help tw-text-blue">{{ctx.Locale.Tr "settings.password_username_disabled"}}</p>
{{end}}
<div class="required field">
<label for="username">{{ctx.Locale.Tr "username"}}</label>
<input id="username" name="name" value="{{.SignedUser.Name}}" disabled maxlength="40">
</div>
<div class="field {{if .Err_FullName}}error{{end}}">
<label for="full_name">{{ctx.Locale.Tr "settings.full_name"}}</label>
<input id="full_name" name="full_name" value="{{.SignedUser.FullName}}" {{if ($.UserDisabledFeatures.Contains "change_full_name")}}disabled{{end}} maxlength="100">
{{if ($.UserDisabledFeatures.Contains "change_full_name")}}
<p class="help tw-text-blue">{{ctx.Locale.Tr "settings.password_full_name_disabled"}}</p>
{{end}}
<input id="full_name" name="full_name" value="{{.SignedUser.FullName}}" maxlength="100">
</div>
<div class="field {{if .Err_Email}}error{{end}}">
<label>{{ctx.Locale.Tr "email"}}</label>
+4 -46
View File
@@ -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-----');
}
+12 -15
View File
@@ -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.');
}
});
}
}
+27 -25
View File
@@ -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');
});
}
+36 -21
View File
@@ -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,
);
});
}