Federated GPG-based auth #3

Merged
d merged 15 commits from gpg-auth into main 2026-06-12 20:38:00 +00:00
57 changed files with 1202 additions and 737 deletions
+30
View File
@@ -20,4 +20,34 @@ Roadmap:
- integrated video-conferences
- integrated stickers
- integrated NFT assets, crypto-wallets
TRANSLATIONS:
cs-CZ
de-DE
el-GR
en-US
es-ES
fa-IR
fi-FI
fr-FR
ga-IE
hu-HU
id-ID
is-IS
it-IT
ja-JP
ko-KR
lv-LV
nl-NL
pl-PL
pt-BR
pt-PT
ru-RU
si-LK
sk-SK
sv-SE
tr-TR
uk-UA
zh-CN
zh-TW
-->
+11
View File
@@ -120,6 +120,17 @@ func (err ErrGPGInvalidTokenSignature) Error() string {
return "the provided signature does not sign the token with the provided key"
}
type ErrGPGLastKey struct{}
func (e ErrGPGLastKey) Error() string {
return "cannot delete last GPG key"
}
func IsErrGPGLastKey(err error) bool {
_, ok := err.(ErrGPGLastKey)
return ok
}
// ErrGPGKeyParsing represents a "ErrGPGKeyParsing" kind of error.
type ErrGPGKeyParsing struct {
ParseError error
+37
View File
@@ -228,12 +228,49 @@ func DeleteGPGKey(ctx context.Context, doer *user_model.User, id int64) (err err
return fmt.Errorf("GetPublicKeyByID: %w", err)
}
count, err := db.GetEngine(ctx).Where("owner_id = ?", doer.ID).Count(new(GPGKey))
if err != nil {
return err
}
if count <= 1 {
return ErrGPGLastKey{}
}
return db.WithTx(ctx, func(ctx context.Context) error {
_, err = deleteGPGKey(ctx, key.KeyID)
return err
})
}
// ExtractGPGKeyIdentity returns the primary name and email from the first UID of an armored key
func ExtractGPGKeyIdentity(armoredKey string) (name, email string, err error) {
keys, err := CheckArmoredGPGKeyString(armoredKey)
if err != nil {
return "", "", err
}
if len(keys) == 0 {
return "", "", fmt.Errorf("no keys found")
}
for _, uid := range keys[0].Identities {
if uid.UserId != nil {
return uid.UserId.Name, uid.UserId.Email, nil
}
}
return "", "", fmt.Errorf("no identity found in key")
}
// ExtractGPGKeyID returns the key ID string from an armored key
func ExtractGPGKeyID(armoredKey string) (string, error) {
keys, err := CheckArmoredGPGKeyString(armoredKey)
if err != nil {
return "", err
}
if len(keys) == 0 {
return "", fmt.Errorf("no keys found")
}
return fmt.Sprintf("%016X", keys[0].PrimaryKey.KeyId), nil
}
func FindGPGKeyWithSubKeys(ctx context.Context, keyID string) ([]*GPGKey, error) {
return db.Find[GPGKey](ctx, FindGPGKeyOptions{
KeyID: keyID,
+136 -80
View File
@@ -5,94 +5,150 @@ package asymkey
import (
"context"
"fmt"
"strconv"
"time"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/base"
"gitea.dev/modules/log"
"gitea.dev/modules/cache"
)
// This file provides functions relating verifying gpg keys
const nonceTTL = 5 * time.Minute
// VerifyGPGKey marks a GPG key as verified
func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature string) (string, error) {
return db.WithTx2(ctx, func(ctx context.Context) (string, error) {
key := new(GPGKey)
has, err := db.GetEngine(ctx).Where("owner_id = ? AND key_id = ?", ownerID, keyID).Get(key)
if err != nil {
return "", err
} else if !has {
return "", ErrGPGKeyNotExist{}
}
if err := key.LoadSubKeys(ctx); err != nil {
return "", err
}
sig, err := ExtractSignature(signature)
if err != nil {
return "", ErrGPGInvalidTokenSignature{
ID: key.KeyID,
Wrapped: err,
}
}
signer, err := hashAndVerifyWithSubKeys(sig, token, key)
if err != nil {
return "", ErrGPGInvalidTokenSignature{
ID: key.KeyID,
Wrapped: err,
}
}
if signer == nil {
signer, err = hashAndVerifyWithSubKeys(sig, token+"\n", key)
if err != nil {
return "", ErrGPGInvalidTokenSignature{
ID: key.KeyID,
Wrapped: err,
}
}
}
if signer == nil {
signer, err = hashAndVerifyWithSubKeys(sig, token+"\n\n", key)
if err != nil {
return "", ErrGPGInvalidTokenSignature{
ID: key.KeyID,
Wrapped: err,
}
}
}
if signer == nil {
log.Debug("VerifyGPGKey failed: no signer")
return "", ErrGPGInvalidTokenSignature{
ID: key.KeyID,
}
}
if signer.PrimaryKeyID != key.KeyID && signer.KeyID != key.KeyID {
return "", ErrGPGKeyNotExist{}
}
key.Verified = true
if _, err := db.GetEngine(ctx).ID(key.ID).Cols("verified").Update(key); err != nil {
return "", err
}
return key.KeyID, nil
})
func nonceKey(nonce string) string {
return "gpg_nonce:" + nonce
}
// VerificationToken returns token for the user that will be valid in minutes (time)
func VerificationToken(user *user_model.User, minutes int) string {
return base.EncodeSha256(
time.Now().Truncate(1*time.Minute).Add(time.Duration(minutes)*time.Minute).Format(
time.RFC1123Z) + ":" +
user.CreatedUnix.Format(time.RFC1123Z) + ":" +
user.Name + ":" +
user.Email + ":" +
strconv.FormatInt(user.ID, 10))
// VerifyNonce returns true if nonce is valid (not seen before, not expired)
// and marks it as used
func VerifyNonce(nonce string) bool {
if len(nonce) != 64 {
return false
}
// first 8 chars are timestamp
tsHex := nonce[:8]
ts, err := strconv.ParseInt(tsHex, 16, 64)
if err != nil {
return false
}
now := time.Now().Unix()
age := now - ts
if age < 0 || age > int64(nonceTTL.Seconds()) {
return false // expired or future timestamp
}
key := nonceKey(nonce)
_, exists := cache.GetCache().Get(key)
if exists {
return false
}
cache.GetCache().Put(key, "1", int64(nonceTTL.Seconds()))
return true
}
// VerifyGPGSignature verifies a detached GPG signature against a nonce,
// extracts the key ID, looks up the key in the DB and returns the owning user.
func VerifyGPGSignature(ctx context.Context, nonce, signature string) (*user_model.User, error) {
if !VerifyNonce(nonce) {
return nil, ErrGPGInvalidTokenSignature{}
}
sig, err := ExtractSignature(signature)
if err != nil {
return nil, ErrGPGInvalidTokenSignature{Wrapped: err}
}
if sig.IssuerKeyId == nil {
return nil, ErrGPGInvalidTokenSignature{}
}
keyID := fmt.Sprintf("%016X", *sig.IssuerKeyId)
key := new(GPGKey)
has, err := db.GetEngine(ctx).Where("key_id = ?", keyID).Get(key)
if err != nil {
return nil, err
} else if !has {
return nil, ErrGPGKeyNotExist{}
}
if err := key.LoadSubKeys(ctx); err != nil {
return nil, err
}
signer, err := hashAndVerifyWithSubKeys(sig, nonce, key)
if signer == nil {
signer, err = hashAndVerifyWithSubKeys(sig, nonce+"\n", key)
}
if signer == nil {
signer, err = hashAndVerifyWithSubKeys(sig, nonce+"\n\n", key)
}
if signer == nil {
return nil, ErrGPGInvalidTokenSignature{ID: key.KeyID, Wrapped: err}
}
user, err := user_model.GetUserByID(ctx, key.OwnerID)
if err != nil {
return nil, err
}
return user, nil
}
// VerifyGPGSignatureWithKey verifies a detached GPG signature against a nonce
// using a provided armored public key, without requiring it to be in the DB.
func VerifyGPGSignatureWithKey(nonce, signature, armoredKey string) (bool, error) {
if !VerifyNonce(nonce) {
return false, ErrGPGInvalidTokenSignature{}
}
sig, err := ExtractSignature(signature)
if err != nil {
return false, ErrGPGInvalidTokenSignature{Wrapped: err}
}
keys, err := CheckArmoredGPGKeyString(armoredKey)
if err != nil {
return false, err
}
for _, k := range keys {
content, err := Base64EncPubKey(k.PrimaryKey)
if err != nil {
continue
}
gpgKey := &GPGKey{
KeyID: fmt.Sprintf("%016X", k.PrimaryKey.KeyId),
Content: content,
CanSign: k.PrimaryKey.CanSign(),
}
for _, sub := range k.Subkeys {
subContent, err := Base64EncPubKey(sub.PublicKey)
if err != nil {
continue
}
gpgKey.SubsKey = append(gpgKey.SubsKey, &GPGKey{
KeyID: fmt.Sprintf("%016X", sub.PublicKey.KeyId),
Content: subContent,
CanSign: sub.PublicKey.CanSign(),
})
}
signer, err := hashAndVerifyWithSubKeys(sig, nonce, gpgKey)
if signer == nil {
signer, err = hashAndVerifyWithSubKeys(sig, nonce+"\n", gpgKey)
}
if signer == nil {
signer, err = hashAndVerifyWithSubKeys(sig, nonce+"\n\n", gpgKey)
}
if signer != nil {
return true, nil
}
}
return false, ErrGPGInvalidTokenSignature{}
}
+15
View File
@@ -5,9 +5,13 @@ package asymkey
import (
"context"
"strconv"
"strings"
"time"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/base"
"gitea.dev/modules/log"
"github.com/42wim/sshsig"
@@ -45,3 +49,14 @@ func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signat
return key.Fingerprint, nil
})
}
// VerificationToken returns token for the user that will be valid in minutes (time)
func VerificationToken(user *user_model.User, minutes int) string {
return base.EncodeSha256(
time.Now().Truncate(1*time.Minute).Add(time.Duration(minutes)*time.Minute).Format(
time.RFC1123Z) + ":" +
user.CreatedUnix.Format(time.RFC1123Z) + ":" +
user.Name + ":" +
user.Email + ":" +
strconv.FormatInt(user.ID, 10))
}
+4
View File
@@ -57,6 +57,8 @@ type CreateGPGKeyOption struct {
ArmoredKey string `json:"armored_public_key" binding:"Required"`
// An optional armored signature for the GPG key
Signature string `json:"armored_signature,omitempty"`
// Nonce for signature
Nonce string `json:"nonce,omitempty"`
}
// VerifyGPGKeyOption options verifies user GPG key
@@ -68,4 +70,6 @@ type VerifyGPGKeyOption struct {
KeyID string `json:"key_id" binding:"Required"`
// The armored signature to verify the GPG key
Signature string `json:"armored_signature" binding:"Required"`
// Nonce for signature
Nonce string `json:"nonce,omitempty"`
}
+16 -1
View File
@@ -341,6 +341,9 @@
"auth.invalid_code": "Tento potvrzující kód je neplatný nebo mu vypršela platnost.",
"auth.invalid_code_forgot_password": "Váš potvrzovací kód je neplatný nebo mu vypršela platnost. <a href=\"%s\">Klikněte zde</a> pro vytvoření nového kódu.",
"auth.invalid_password": "Vaše heslo se neshoduje s heslem, které bylo použito k vytvoření účtu.",
"auth.gpg_invalid_token_signature": "Neplatný GPG podpis nebo nonce",
"auth.invalid_gpg_identity": "Nepodařilo se extrahovat identitu z GPG klíče",
"auth.invalid_gpg_key_email": "Nepodařilo se extrahovat e-mailovou adresu z GPG klíče",
"auth.reset_password_helper": "Obnovit účet",
"auth.reset_password_wrong_user": "Jste přihlášen/a jako %s, ale odkaz pro obnovení účtu je pro %s",
"auth.password_too_short": "Délka hesla musí být minimálně %d znaků.",
@@ -685,6 +688,7 @@
"settings.gpg_token_required": "Musíte zadat podpis pro níže uvedený token",
"settings.gpg_token_help": "Podpis můžete vygenerovat pomocí:",
"settings.gpg_token_signature": "Zakódovaný podpis GPG",
"settings.gpg_last_key_delete": "Nelze smazat váš jediný klíč",
"settings.key_signature_gpg_placeholder": "Začíná s „-----BEGIN PGP SIGNATURE-----“",
"settings.verify_gpg_key_success": "GPG klíč „%s“ byl ověřen.",
"settings.ssh_key_verified": "Ověřený klíč",
@@ -3057,6 +3061,17 @@
"gpg.error.no_gpg_keys_found": "V databázi nebyl nalezen žádný známý klíč pro tento podpis",
"gpg.error.not_signed_commit": "Nepodepsaná revize",
"gpg.error.failed_retrieval_gpg_keys": "Nelze získat žádný klíč propojený s účtem přispěvatele",
"gpg.signin.title": "Přihlásit se",
"gpg.signin.nonce_label": "Podepište tento nonce svým soukromým klíčem",
"gpg.signin.command_hint": "Spusťte tento příkaz a vložte výstup níže:",
"gpg.signin.copy": "Kopírovat",
"gpg.signin.signed_output": "Podepsaný výstup",
"gpg.signin.submit": "Přihlásit se",
"gpg.signup.title": "Vytvořit účet",
"gpg.signup.paste_key": "Vložte svůj veřejný GPG klíč",
"gpg.signup.proceed": "Pokračovat",
"gpg.signup.submit": "Ověřit a vytvořit účet",
"gpg.signup.already_have_account": "Již máte účet?",
"units.unit": "Jednotka",
"units.error.no_unit_allowed_repo": "Nejste oprávněni přistupovat k žádné části tohoto repozitáře.",
"units.error.unit_not_allowed": "Nejste oprávněni přistupovat k této části repozitáře.",
@@ -3307,4 +3322,4 @@
"git.filemode.executable_file": "Spustitelný soubor",
"git.filemode.symbolic_link": "Symbolický odkaz",
"git.filemode.submodule": "Submodul"
}
}
+16 -1
View File
@@ -334,6 +334,9 @@
"auth.invalid_code": "Dein Bestätigungs-Code ist ungültig oder abgelaufen.",
"auth.invalid_code_forgot_password": "Dein Bestätigungscode ist ungültig oder abgelaufen. Klicke <a href=\"%s\">hier</a>, um eine neue Sitzung zu starten.",
"auth.invalid_password": "Ihr Passwort stimmt nicht mit dem Passwort überein, das zur Erstellung des Kontos verwendet wurde.",
"auth.gpg_invalid_token_signature": "Ungültige GPG-Signatur oder Nonce",
"auth.invalid_gpg_identity": "Identität konnte nicht aus dem GPG-Schlüssel extrahiert werden",
"auth.invalid_gpg_key_email": "E-Mail-Adresse konnte nicht aus dem GPG-Schlüssel extrahiert werden",
"auth.reset_password_helper": "Konto wiederherstellen",
"auth.reset_password_wrong_user": "Du bist angemeldet als %s, aber der Link zur Kontowiederherstellung ist für %s",
"auth.password_too_short": "Das Passwort muss mindestens %d Zeichen lang sein.",
@@ -676,6 +679,7 @@
"settings.gpg_token_required": "Du musst eine Signatur für das folgende Token angeben",
"settings.gpg_token_help": "Du kannst eine Signatur wie folgt generieren:",
"settings.gpg_token_signature": "GPG Textsignatur (armored signature)",
"settings.gpg_last_key_delete": "Du kannst deinen einzigen Schlüssel nicht löschen",
"settings.key_signature_gpg_placeholder": "Beginnt mit '-----BEGIN PGP SIGNATURE-----'",
"settings.verify_gpg_key_success": "GPG-Schlüssel \"%s\" wurde verifiziert.",
"settings.ssh_key_verified": "Verifizierter Schlüssel",
@@ -3005,6 +3009,17 @@
"gpg.error.no_gpg_keys_found": "Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden",
"gpg.error.not_signed_commit": "Kein signierter Commit",
"gpg.error.failed_retrieval_gpg_keys": "Fehler beim Abrufen eines Keys des Commiter-Kontos",
"gpg.signin.title": "Anmelden",
"gpg.signin.nonce_label": "Signiere diesen Nonce mit deinem privaten Schlüssel",
"gpg.signin.command_hint": "Führe diesen Befehl aus und füge die Ausgabe unten ein:",
"gpg.signin.copy": "Kopieren",
"gpg.signin.signed_output": "Signierte Ausgabe",
"gpg.signin.submit": "Anmelden",
"gpg.signup.title": "Konto erstellen",
"gpg.signup.paste_key": "Füge deinen öffentlichen GPG-Schlüssel ein",
"gpg.signup.proceed": "Weiter",
"gpg.signup.submit": "Verifizieren & Konto erstellen",
"gpg.signup.already_have_account": "Bereits ein Konto?",
"units.unit": "Einheit",
"units.error.no_unit_allowed_repo": "Du hast keine Berechtigung, auf etwas in diesem Repository zuzugreifen.",
"units.error.unit_not_allowed": "Du hast keine Berechtigung, um auf diesen Repository-Bereich zuzugreifen.",
@@ -3249,4 +3264,4 @@
"git.filemode.executable_file": "Ausführbare Datei",
"git.filemode.symbolic_link": "Softlink",
"git.filemode.submodule": "Submodul"
}
}
+17 -2
View File
@@ -3000,5 +3000,20 @@
"git.filemode.directory": "Φάκελος",
"git.filemode.normal_file": "Κανονικό αρχείο",
"git.filemode.executable_file": "Εκτελέσιμο αρχείο",
"git.filemode.submodule": "Υπομονάδα"
}
"git.filemode.submodule": "Υπομονάδα",
"auth.gpg_invalid_token_signature": "Μη έγκυρη υπογραφή GPG ή nonce",
"auth.invalid_gpg_identity": "Δεν ήταν δυνατή η εξαγωγή ταυτότητας από το κλειδί GPG",
"auth.invalid_gpg_key_email": "Δεν ήταν δυνατή η εξαγωγή διεύθυνσης email από το κλειδί GPG",
"settings.gpg_last_key_delete": "Δεν μπορείτε να διαγράψετε το μοναδικό σας κλειδί",
"gpg.signin.title": "Σύνδεση",
"gpg.signin.nonce_label": "Υπογράψτε αυτό το nonce με το ιδιωτικό σας κλειδί",
"gpg.signin.command_hint": "Εκτελέστε αυτήν την εντολή και επικολλήστε το αποτέλεσμα παρακάτω:",
"gpg.signin.copy": "Αντιγραφή",
"gpg.signin.signed_output": "Υπογεγραμμένη έξοδος",
"gpg.signin.submit": "Σύνδεση",
"gpg.signup.title": "Δημιουργία λογαριασμού",
"gpg.signup.paste_key": "Επικολλήστε το δημόσιο κλειδί GPG σας",
"gpg.signup.proceed": "Συνέχεια",
"gpg.signup.submit": "Επαλήθευση & Δημιουργία λογαριασμού",
"gpg.signup.already_have_account": "Έχετε ήδη λογαριασμό;"
}
+16 -1
View File
@@ -407,6 +407,9 @@
"auth.invalid_code": "Your confirmation code is invalid or has expired.",
"auth.invalid_code_forgot_password": "Your confirmation code is invalid or has expired. Click <a href=\"%s\">here</a> to start a new session.",
"auth.invalid_password": "Your password does not match the password that was used to create the account.",
"auth.gpg_invalid_token_signature": "Invalid GPG signature or nonce",
"auth.invalid_gpg_identity": "Could not extract identity from GPG key",
"auth.invalid_gpg_key_email": "Could not extract email address from GPG key",
"auth.reset_password_helper": "Recover Account",
"auth.reset_password_wrong_user": "You are signed in as %s, but the account recovery link is meant for %s",
"auth.password_too_short": "Password length cannot be less than %d characters.",
@@ -793,6 +796,7 @@
"settings.gpg_token": "Token",
"settings.gpg_token_help": "You can generate a signature using:",
"settings.gpg_token_signature": "Armored GPG signature",
"settings.gpg_last_key_delete": "Cannot delete your only key",
"settings.key_signature_gpg_placeholder": "Begins with '-----BEGIN PGP SIGNATURE-----'",
"settings.verify_gpg_key_success": "GPG key \"%s\" has been verified.",
"settings.ssh_key_verified": "Verified Key",
@@ -3503,6 +3507,17 @@
"gpg.error.failed_retrieval_gpg_keys": "Failed to retrieve any key attached to the committer's account",
"gpg.error.probable_bad_signature": "WARNING! Although there is a key with this ID in the database, it does not verify this commit! This commit is SUSPICIOUS.",
"gpg.error.probable_bad_default_signature": "WARNING! Although the default key has this ID, it does not verify this commit! This commit is SUSPICIOUS.",
"gpg.signin.title": "Sign In",
"gpg.signin.nonce_label": "Sign this nonce with your private key",
"gpg.signin.command_hint": "Run this command, then paste the output below:",
"gpg.signin.copy": "Copy",
"gpg.signin.signed_output": "Signed output",
"gpg.signin.submit": "Sign In",
"gpg.signup.title": "Create your account",
"gpg.signup.paste_key": "Paste your GPG public key",
"gpg.signup.proceed": "Proceed",
"gpg.signup.submit": "Verify & Create Account",
"gpg.signup.already_have_account": "Already have an account?",
"units.unit": "Unit",
"units.error.no_unit_allowed_repo": "You are not allowed to access any section of this repository.",
"units.error.unit_not_allowed": "You are not allowed to access this repository section.",
@@ -3888,4 +3903,4 @@
"actions.general.cross_repo_selected": "Selected repositories",
"actions.general.cross_repo_target_repos": "Target Repositories",
"actions.general.cross_repo_add": "Add Target Repository"
}
}
+16 -1
View File
@@ -300,6 +300,9 @@
"auth.invalid_code": "Su código de confirmación no es válido o ha caducado.",
"auth.invalid_code_forgot_password": "Su código de confirmación no es válido o ha caducado. Haga clic <a href=\"%s\">aquí</a> para iniciar una nueva sesión.",
"auth.invalid_password": "Su contraseña no coincide con la contraseña utilizada para crear la cuenta.",
"auth.gpg_invalid_token_signature": "Firma GPG o nonce inválido",
"auth.invalid_gpg_identity": "No se pudo extraer la identidad de la clave GPG",
"auth.invalid_gpg_key_email": "No se pudo extraer el correo electrónico de la clave GPG",
"auth.reset_password_helper": "Recuperar cuenta",
"auth.reset_password_wrong_user": "Has iniciado sesión como %s, pero el enlace de recuperación de la cuenta está destinado a %s",
"auth.password_too_short": "La longitud de la contraseña no puede ser menor a %d caracteres.",
@@ -608,6 +611,7 @@
"settings.gpg_token_required": "Debe proporcionar una firma para el token de abajo",
"settings.gpg_token_help": "Puede generar una firma de la siguiente manera:",
"settings.gpg_token_signature": "Firma GPG armadura",
"settings.gpg_last_key_delete": "No puedes eliminar tu única clave",
"settings.key_signature_gpg_placeholder": "Comienza con '-----BEGIN PGP SIGNATURE-----'",
"settings.verify_gpg_key_success": "La clave GPG \"%s\" ha sido verificada.",
"settings.ssh_key_verified": "Clave verificada",
@@ -2732,6 +2736,17 @@
"gpg.error.no_gpg_keys_found": "No se encontró ninguna clave conocida en la base de datos para esta firma",
"gpg.error.not_signed_commit": "No es un commit firmado",
"gpg.error.failed_retrieval_gpg_keys": "No se pudo recuperar cualquier clave adjunta a la cuenta del committer",
"gpg.signin.title": "Iniciar sesión",
"gpg.signin.nonce_label": "Firma este nonce con tu clave privada",
"gpg.signin.command_hint": "Ejecuta este comando y pega el resultado abajo:",
"gpg.signin.copy": "Copiar",
"gpg.signin.signed_output": "Salida firmada",
"gpg.signin.submit": "Iniciar sesión",
"gpg.signup.title": "Crear tu cuenta",
"gpg.signup.paste_key": "Pega tu clave pública GPG",
"gpg.signup.proceed": "Continuar",
"gpg.signup.submit": "Verificar y crear cuenta",
"gpg.signup.already_have_account": "¿Ya tienes una cuenta?",
"units.unit": "Unidad",
"units.error.no_unit_allowed_repo": "No tiene permisos para acceder a ninguna sección de este repositorio.",
"units.error.unit_not_allowed": "No tiene permisos para acceder a esta sección del repositorio.",
@@ -2960,4 +2975,4 @@
"git.filemode.executable_file": "Archivo ejecutable",
"git.filemode.symbolic_link": "Enlace simbólico",
"git.filemode.submodule": "Submódulo"
}
}
+17 -2
View File
@@ -2228,5 +2228,20 @@
"actions.runners.status.active": "فعال",
"actions.runners.version": "نسخه",
"actions.runs.commit": "کامیت",
"git.filemode.symbolic_link": "پیوند نمادین"
}
"git.filemode.symbolic_link": "پیوند نمادین",
"auth.gpg_invalid_token_signature": "امضای GPG یا nonce نامعتبر است",
"auth.invalid_gpg_identity": "استخراج هویت از کلید GPG امکان‌پذیر نبود",
"auth.invalid_gpg_key_email": "استخراج آدرس ایمیل از کلید GPG امکان‌پذیر نبود",
"settings.gpg_last_key_delete": "نمی‌توانید تنها کلید خود را حذف کنید",
"gpg.signin.title": "ورود",
"gpg.signin.nonce_label": "این nonce را با کلید خصوصی خود امضا کنید",
"gpg.signin.command_hint": "این دستور را اجرا کنید و خروجی را در زیر جای‌گذاری کنید:",
"gpg.signin.copy": "کپی",
"gpg.signin.signed_output": "خروجی امضاشده",
"gpg.signin.submit": "ورود",
"gpg.signup.title": "ایجاد حساب کاربری",
"gpg.signup.paste_key": "کلید عمومی GPG خود را جای‌گذاری کنید",
"gpg.signup.proceed": "ادامه",
"gpg.signup.submit": "تأیید و ایجاد حساب",
"gpg.signup.already_have_account": "قبلاً حساب دارید؟"
}
+17 -2
View File
@@ -1474,5 +1474,20 @@
"actions.runners.labels": "Tunnisteet",
"actions.runners.task_list.run": "Suorita",
"actions.runners.task_list.repository": "Repo",
"actions.runners.version": "Versio"
}
"actions.runners.version": "Versio",
"auth.gpg_invalid_token_signature": "Virheellinen GPG-allekirjoitus tai nonce",
"auth.invalid_gpg_identity": "Henkilöllisyyttä ei voitu poimia GPG-avaimesta",
"auth.invalid_gpg_key_email": "Sähköpostiosoitetta ei voitu poimia GPG-avaimesta",
"settings.gpg_last_key_delete": "Et voi poistaa ainoaa avainta",
"gpg.signin.title": "Kirjaudu sisään",
"gpg.signin.nonce_label": "Allekirjoita tämä nonce yksityisellä avaimellasi",
"gpg.signin.command_hint": "Suorita tämä komento ja liitä tulos alle:",
"gpg.signin.copy": "Kopioi",
"gpg.signin.signed_output": "Allekirjoitettu tulos",
"gpg.signin.submit": "Kirjaudu sisään",
"gpg.signup.title": "Luo tili",
"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?"
}
+16 -1
View File
@@ -407,6 +407,9 @@
"auth.invalid_code": "Votre code de confirmation est invalide ou a expiré.",
"auth.invalid_code_forgot_password": "Votre code de confirmation est invalide ou a expiré. Cliquez <a href=\"%s\">ici</a> pour démarrer une nouvelle session.",
"auth.invalid_password": "Votre mot de passe ne correspond pas à celui utilisé pour créer le compte.",
"auth.gpg_invalid_token_signature": "Signature GPG ou nonce invalide",
"auth.invalid_gpg_identity": "Impossible d'extraire l'identité de la clé GPG",
"auth.invalid_gpg_key_email": "Impossible d'extraire l'adresse e-mail de la clé GPG",
"auth.reset_password_helper": "Récupérer un compte",
"auth.reset_password_wrong_user": "Vous êtes connecté en tant que %s, mais le lien de récupération est pour %s",
"auth.password_too_short": "Le mot de passe doit contenir %d caractères minimum.",
@@ -793,6 +796,7 @@
"settings.gpg_token": "Jeton",
"settings.gpg_token_help": "Vous pouvez générer une signature en utilisant :",
"settings.gpg_token_signature": "Signature GPG renforcée",
"settings.gpg_last_key_delete": "Impossible de supprimer votre seule clé",
"settings.key_signature_gpg_placeholder": "Commence par '-----BEGIN PGP SIGNATURE-----'",
"settings.verify_gpg_key_success": "La clé GPG \"%s\" a été vérifiée.",
"settings.ssh_key_verified": "Clé vérifiée",
@@ -3498,6 +3502,17 @@
"gpg.error.failed_retrieval_gpg_keys": "Impossible de récupérer la clé liée au compte de l'auteur",
"gpg.error.probable_bad_signature": "AVERTISSEMENT ! Bien quil y ait une clé avec cet ID dans la base de données, elle ne vérifie pas cette révision ! Cette révision est SUSPECTE.",
"gpg.error.probable_bad_default_signature": "AVERTISSEMENT ! Bien que la clé par défaut ait cet ID, elle ne vérifie pas cette révision ! Cette révision est SUSPECTE.",
"gpg.signin.title": "Se connecter",
"gpg.signin.nonce_label": "Signez ce nonce avec votre clé privée",
"gpg.signin.command_hint": "Exécutez cette commande puis collez le résultat ci-dessous :",
"gpg.signin.copy": "Copier",
"gpg.signin.signed_output": "Sortie signée",
"gpg.signin.submit": "Se connecter",
"gpg.signup.title": "Créer votre compte",
"gpg.signup.paste_key": "Collez votre clé publique GPG",
"gpg.signup.proceed": "Continuer",
"gpg.signup.submit": "Vérifier & créer le compte",
"gpg.signup.already_have_account": "Vous avez déjà un compte ?",
"units.unit": "Ressource",
"units.error.no_unit_allowed_repo": "Vous n'êtes pas autorisé à accéder à n'importe quelle section de ce dépôt.",
"units.error.unit_not_allowed": "Vous n'êtes pas autorisé à accéder à cette section du dépôt.",
@@ -3866,4 +3881,4 @@
"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"
}
}
+17 -2
View File
@@ -3879,5 +3879,20 @@
"actions.general.cross_repo_desc": "Ceadaigh rochtain (léamh amháin) a bheith ag na stórtha uile san úinéir seo ar na stórtha roghnaithe le GITEA_TOKEN agus poist Gníomhartha á reáchtáil.",
"actions.general.cross_repo_selected": "Stórtha roghnaithe",
"actions.general.cross_repo_target_repos": "Stórtha Spriocdhírithe",
"actions.general.cross_repo_add": "Cuir Stór Sprioc leis"
}
"actions.general.cross_repo_add": "Cuir Stór Sprioc leis",
"auth.gpg_invalid_token_signature": "Síniú GPG nó nonce neamhbhailí",
"auth.invalid_gpg_identity": "Níorbh fhéidir céannacht a bhaint as an eochair GPG",
"auth.invalid_gpg_key_email": "Níorbh fhéidir seoladh ríomhphoist a bhaint as an eochair GPG",
"settings.gpg_last_key_delete": "Ní féidir leat d'aon eochair amháin a scriosadh",
"gpg.signin.title": "Sínigh isteach",
"gpg.signin.nonce_label": "Sínigh an nonce seo le d'eochair phríobháideach",
"gpg.signin.command_hint": "Rith an t-ordú seo agus greamaigh an t-aschur thíos:",
"gpg.signin.copy": "Cóipeáil",
"gpg.signin.signed_output": "Aschur sínithe",
"gpg.signin.submit": "Sínigh isteach",
"gpg.signup.title": "Cruthaigh do chuntas",
"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?"
}
+17 -2
View File
@@ -1383,5 +1383,20 @@
"actions.runners.task_list.repository": "Tároló",
"actions.runners.status.active": "Aktív",
"actions.runners.version": "Verzió",
"git.filemode.symbolic_link": "Szimbolikus hivatkozás"
}
"git.filemode.symbolic_link": "Szimbolikus hivatkozás",
"auth.gpg_invalid_token_signature": "Érvénytelen GPG aláírás vagy nonce",
"auth.invalid_gpg_identity": "Nem sikerült azonosságot kinyerni a GPG kulcsból",
"auth.invalid_gpg_key_email": "Nem sikerült e-mail címet kinyerni a GPG kulcsból",
"settings.gpg_last_key_delete": "Nem törölheted az egyetlen kulcsodat",
"gpg.signin.title": "Bejelentkezés",
"gpg.signin.nonce_label": "Írd alá ezt a nonce-t a privát kulcsoddal",
"gpg.signin.command_hint": "Futtasd ezt a parancsot, majd illeszd be az eredményt alább:",
"gpg.signin.copy": "Másolás",
"gpg.signin.signed_output": "Aláírt kimenet",
"gpg.signin.submit": "Bejelentkezés",
"gpg.signup.title": "Fiók létrehozása",
"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?"
}
+17 -2
View File
@@ -1198,5 +1198,20 @@
"actions.variables.update.success": "Variabel telah diedit.",
"projects.type-1.display_name": "Proyek Individu",
"projects.type-2.display_name": "Proyek Repositori",
"projects.type-3.display_name": "Proyek Organisasi"
}
"projects.type-3.display_name": "Proyek Organisasi",
"auth.gpg_invalid_token_signature": "Tanda tangan GPG atau nonce tidak valid",
"auth.invalid_gpg_identity": "Gagal mengekstrak identitas dari kunci GPG",
"auth.invalid_gpg_key_email": "Gagal mengekstrak alamat email dari kunci GPG",
"settings.gpg_last_key_delete": "Anda tidak dapat menghapus satu-satunya kunci",
"gpg.signin.title": "Masuk",
"gpg.signin.nonce_label": "Tandatangani nonce ini dengan kunci pribadi Anda",
"gpg.signin.command_hint": "Jalankan perintah ini lalu tempel hasilnya di bawah:",
"gpg.signin.copy": "Salin",
"gpg.signin.signed_output": "Output yang ditandatangani",
"gpg.signin.submit": "Masuk",
"gpg.signup.title": "Buat akun",
"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?"
}
+17 -2
View File
@@ -1116,5 +1116,20 @@
"actions.runners.task_list.commit": "Framlag",
"actions.runners.status.active": "Virkt",
"actions.runners.version": "Útgáfa",
"actions.runs.commit": "Framlag"
}
"actions.runs.commit": "Framlag",
"auth.gpg_invalid_token_signature": "Ógilt GPG undirskrift eða nonce",
"auth.invalid_gpg_identity": "Tókst ekki að draga út auðkenni úr GPG lykli",
"auth.invalid_gpg_key_email": "Tókst ekki að draga út netfang úr GPG lykli",
"settings.gpg_last_key_delete": "Þú getur ekki eytt eina lyklinum þínum",
"gpg.signin.title": "Skrá inn",
"gpg.signin.nonce_label": "Undirritaðu þetta nonce með leynilyklinum þínum",
"gpg.signin.command_hint": "Keyrðu þessa skipun og límdu niðurstöðuna hér að neðan:",
"gpg.signin.copy": "Afrita",
"gpg.signin.signed_output": "Undirrituð úttak",
"gpg.signin.submit": "Skrá inn",
"gpg.signup.title": "Búa til reikning",
"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?"
}
+17 -2
View File
@@ -2377,5 +2377,20 @@
"actions.runners.task_list.run": "Esegui",
"actions.runners.status.active": "Attivo",
"actions.runners.version": "Versione",
"git.filemode.symbolic_link": "Link Simbolico"
}
"git.filemode.symbolic_link": "Link Simbolico",
"auth.gpg_invalid_token_signature": "Firma GPG o nonce non validi",
"auth.invalid_gpg_identity": "Impossibile estrarre l'identità dalla chiave GPG",
"auth.invalid_gpg_key_email": "Impossibile estrarre l'indirizzo email dalla chiave GPG",
"settings.gpg_last_key_delete": "Non puoi eliminare l'unica tua chiave",
"gpg.signin.title": "Accedi",
"gpg.signin.nonce_label": "Firma questo nonce con la tua chiave privata",
"gpg.signin.command_hint": "Esegui questo comando e incolla l'output qui sotto:",
"gpg.signin.copy": "Copia",
"gpg.signin.signed_output": "Output firmato",
"gpg.signin.submit": "Accedi",
"gpg.signup.title": "Crea il tuo account",
"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?"
}
+5 -1
View File
@@ -407,6 +407,9 @@
"auth.invalid_code": "確認コードが無効か期限切れです。",
"auth.invalid_code_forgot_password": "確認コードは無効または期限切れです。 新しいセッションを開始するには<a href=\"%s\">ここ</a>をクリックしてください。",
"auth.invalid_password": "アカウントの作成に使用されたパスワードと一致しません。",
"auth.gpg_invalid_token_signature": "GPG署名またはnonceが無効です",
"auth.invalid_gpg_identity": "GPGキーからIDを抽出できませんでした",
"auth.invalid_gpg_key_email": "GPGキーからメールアドレスを抽出できませんでした",
"auth.reset_password_helper": "アカウント回復",
"auth.reset_password_wrong_user": "あなたは %s でサインイン中ですが、アカウント回復のリンクは %s のものです。",
"auth.password_too_short": "%d文字未満のパスワードは設定できません。",
@@ -793,6 +796,7 @@
"settings.gpg_token": "トークン",
"settings.gpg_token_help": "署名はこの方法で生成できます:",
"settings.gpg_token_signature": "Armor形式のGPG署名",
"settings.gpg_last_key_delete": "唯一の鍵は削除できません",
"settings.key_signature_gpg_placeholder": "先頭は '-----BEGIN PGP SIGNATURE-----'",
"settings.verify_gpg_key_success": "GPG鍵 \"%s\" を確認しました。",
"settings.ssh_key_verified": "確認済みの鍵",
@@ -3862,4 +3866,4 @@
"actions.general.cross_repo_selected": "選択したリポジトリ",
"actions.general.cross_repo_target_repos": "対象リポジトリ",
"actions.general.cross_repo_add": "対象リポジトリの追加"
}
}
+16 -1
View File
@@ -407,6 +407,9 @@
"auth.invalid_code": "검증 코드가 유효하지 않거나 만료되었습니다.",
"auth.invalid_code_forgot_password": "확인 코드가 유효하지 않거나 만료되었습니다. <a href=\"%s\">여기</a>를 눌러 새로운 세션을 시작하세요.",
"auth.invalid_password": "비밀번호가 계정을 만들 때 사용한 비밀번호와 일치하지 않습니다.",
"auth.gpg_invalid_token_signature": "유효하지 않은 GPG 서명 또는 nonce",
"auth.invalid_gpg_identity": "GPG 키에서 신원을 추출할 수 없습니다",
"auth.invalid_gpg_key_email": "GPG 키에서 이메일 주소를 추출할 수 없습니다",
"auth.reset_password_helper": "계정 복구",
"auth.reset_password_wrong_user": "%s로 로그인했지만 계정 복구 링크는 %s를 위한 것입니다",
"auth.password_too_short": "비밀번호의 길이는 최소 %d 자가 되어야 합니다.",
@@ -793,6 +796,7 @@
"settings.gpg_token": "토큰",
"settings.gpg_token_help": "서명은 다음 명령어로 생성할 수 있습니다:",
"settings.gpg_token_signature": "아머드 GPG 서명",
"settings.gpg_last_key_delete": "유일한 키는 삭제할 수 없습니다",
"settings.key_signature_gpg_placeholder": "'-----BEGIN PGP PUBLIC KEY BLOCK-----' 로 시작",
"settings.verify_gpg_key_success": "GPG 키 \"%s\"가 검증되었습니다.",
"settings.ssh_key_verified": "검증된 키",
@@ -3499,6 +3503,17 @@
"gpg.error.failed_retrieval_gpg_keys": "커미터 계정에 연결된 키를 가져오지 못함",
"gpg.error.probable_bad_signature": "경고! 이 ID를 가진 키가 데이터베이스에 있지만 이 커밋을 검증하지 않습니다! 이 커밋은 의심스럽습니다.",
"gpg.error.probable_bad_default_signature": "경고! 기본 키는 이 ID를 가지지만 이 커밋을 검증하지 않습니다! 이 커밋은 의심스럽습니다.",
"gpg.signin.title": "로그인",
"gpg.signin.nonce_label": "개인 키로 이 nonce에 서명하세요",
"gpg.signin.command_hint": "이 명령을 실행하고 아래에 출력을 붙여넣으세요:",
"gpg.signin.copy": "복사",
"gpg.signin.signed_output": "서명된 출력",
"gpg.signin.submit": "로그인",
"gpg.signup.title": "계정 만들기",
"gpg.signup.paste_key": "GPG 공개 키를 붙여넣으세요",
"gpg.signup.proceed": "계속",
"gpg.signup.submit": "확인 및 계정 생성",
"gpg.signup.already_have_account": "이미 계정이 있으신가요?",
"units.unit": "단위",
"units.error.no_unit_allowed_repo": "이 리포지토리의 어떤 섹션에도 접근할 수 없습니다.",
"units.error.unit_not_allowed": "이 리포지토리 섹션에 접근할 수 없습니다.",
@@ -3879,4 +3894,4 @@
"actions.general.cross_repo_selected": "선택된 리포지토리",
"actions.general.cross_repo_target_repos": "대상 리포지토리",
"actions.general.cross_repo_add": "대상 리포지토리 추가"
}
}
+17 -2
View File
@@ -3042,5 +3042,20 @@
"git.filemode.normal_file": "Parasts fails",
"git.filemode.executable_file": "Izpildāmais fails",
"git.filemode.symbolic_link": "Simboliska saite",
"git.filemode.submodule": "Apakšmodulis"
}
"git.filemode.submodule": "Apakšmodulis",
"auth.gpg_invalid_token_signature": "Nederīgs GPG paraksts vai nonce",
"auth.invalid_gpg_identity": "Neizdevās iegūt identitāti no GPG atslēgas",
"auth.invalid_gpg_key_email": "Neizdevās iegūt e-pasta adresi no GPG atslēgas",
"settings.gpg_last_key_delete": "Nevar dzēst vienīgo atslēgu",
"gpg.signin.title": "Pierakstīties",
"gpg.signin.nonce_label": "Parakstiet šo nonce ar savu privāto atslēgu",
"gpg.signin.command_hint": "Izpildiet šo komandu un ielīmējiet rezultātu zemāk:",
"gpg.signin.copy": "Kopēt",
"gpg.signin.signed_output": "Parakstīts rezultāts",
"gpg.signin.submit": "Pierakstīties",
"gpg.signup.title": "Izveidot kontu",
"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?"
}
+17 -2
View File
@@ -2093,5 +2093,20 @@
"actions.runners.task_list.run": "Uitvoeren",
"actions.runners.task_list.repository": "Opslagplaats",
"actions.runners.status.active": "Actief",
"actions.runners.version": "Versie"
}
"actions.runners.version": "Versie",
"auth.gpg_invalid_token_signature": "Ongeldige GPG-handtekening of nonce",
"auth.invalid_gpg_identity": "Kon identiteit niet extraheren uit GPG-sleutel",
"auth.invalid_gpg_key_email": "Kon e-mailadres niet extraheren uit GPG-sleutel",
"settings.gpg_last_key_delete": "Je kunt je enige sleutel niet verwijderen",
"gpg.signin.title": "Inloggen",
"gpg.signin.nonce_label": "Onderteken deze nonce met je privésleutel",
"gpg.signin.command_hint": "Voer dit commando uit en plak de uitvoer hieronder:",
"gpg.signin.copy": "Kopiëren",
"gpg.signin.signed_output": "Ondertekende uitvoer",
"gpg.signin.submit": "Inloggen",
"gpg.signup.title": "Account aanmaken",
"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?"
}
+17 -2
View File
@@ -2110,5 +2110,20 @@
"actions.runners.task_list.repository": "Repozytorium",
"actions.runners.status.active": "Aktywne",
"actions.runners.version": "Wersja",
"git.filemode.symbolic_link": "Dowiązanie symboliczne"
}
"git.filemode.symbolic_link": "Dowiązanie symboliczne",
"auth.gpg_invalid_token_signature": "Nieprawidłowy podpis GPG lub nonce",
"auth.invalid_gpg_identity": "Nie udało się wyodrębnić tożsamości z klucza GPG",
"auth.invalid_gpg_key_email": "Nie udało się wyodrębnić adresu e-mail z klucza GPG",
"settings.gpg_last_key_delete": "Nie możesz usunąć jedynego klucza",
"gpg.signin.title": "Zaloguj się",
"gpg.signin.nonce_label": "Podpisz ten nonce swoim kluczem prywatnym",
"gpg.signin.command_hint": "Uruchom to polecenie i wklej wynik poniżej:",
"gpg.signin.copy": "Kopiuj",
"gpg.signin.signed_output": "Podpisane wyjście",
"gpg.signin.submit": "Zaloguj się",
"gpg.signup.title": "Utwórz konto",
"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?"
}
+17 -2
View File
@@ -3286,5 +3286,20 @@
"git.filemode.normal_file": "Arquivo normal",
"git.filemode.executable_file": "Arquivo executável",
"git.filemode.symbolic_link": "Link simbólico",
"git.filemode.submodule": "Submódulo"
}
"git.filemode.submodule": "Submódulo",
"auth.gpg_invalid_token_signature": "Assinatura GPG ou nonce inválidos",
"auth.invalid_gpg_identity": "Não foi possível extrair a identidade da chave GPG",
"auth.invalid_gpg_key_email": "Não foi possível extrair o endereço de e-mail da chave GPG",
"settings.gpg_last_key_delete": "Você não pode excluir sua única chave",
"gpg.signin.title": "Entrar",
"gpg.signin.nonce_label": "Assine este nonce com sua chave privada",
"gpg.signin.command_hint": "Execute este comando e cole o resultado abaixo:",
"gpg.signin.copy": "Copiar",
"gpg.signin.signed_output": "Saída assinada",
"gpg.signin.submit": "Entrar",
"gpg.signup.title": "Criar conta",
"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?"
}
+17 -2
View File
@@ -3724,5 +3724,20 @@
"git.filemode.normal_file": "Ficheiro normal",
"git.filemode.executable_file": "Ficheiro executável",
"git.filemode.symbolic_link": "Ligação simbólica",
"git.filemode.submodule": "Submódulo"
}
"git.filemode.submodule": "Submódulo",
"auth.gpg_invalid_token_signature": "Assinatura GPG ou nonce inválidos",
"auth.invalid_gpg_identity": "Não foi possível extrair a identidade da chave GPG",
"auth.invalid_gpg_key_email": "Não foi possível extrair o endereço de e-mail da chave GPG",
"settings.gpg_last_key_delete": "Não pode eliminar a sua única chave",
"gpg.signin.title": "Iniciar sessão",
"gpg.signin.nonce_label": "Assine este nonce com a sua chave privada",
"gpg.signin.command_hint": "Execute este comando e cole o resultado abaixo:",
"gpg.signin.copy": "Copiar",
"gpg.signin.signed_output": "Saída assinada",
"gpg.signin.submit": "Iniciar sessão",
"gpg.signup.title": "Criar conta",
"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?"
}
+16 -1
View File
@@ -307,6 +307,9 @@
"auth.invalid_code": "Код подтверждения недействителен или истёк.",
"auth.invalid_code_forgot_password": "Ваш код подтверждения недействителен или истек. Нажмите <a href=\"%s\">здесь</a> для начала новой сессии.",
"auth.invalid_password": "Ваш пароль не совпадает с паролем, который был использован для создания учётной записи.",
"auth.gpg_invalid_token_signature": "Неверная GPG-подпись или nonce",
"auth.invalid_gpg_identity": "Не удалось извлечь идентификатор из GPG-ключа",
"auth.invalid_gpg_key_email": "Не удалось извлечь адрес электронной почты из GPG-ключа",
"auth.reset_password_helper": "Восстановить аккаунт",
"auth.reset_password_wrong_user": "Вы вошли как %s, но ссылка для восстановления учётной записи предназначена для %s",
"auth.password_too_short": "Пароль не может быть короче %d символов.",
@@ -618,6 +621,7 @@
"settings.gpg_token": "Токен",
"settings.gpg_token_help": "Вы можете сгенерировать подпись с помощью:",
"settings.gpg_token_signature": "Текстовая подпись GPG",
"settings.gpg_last_key_delete": "Невозможно удалить единственный ключ",
"settings.key_signature_gpg_placeholder": "Начинается с '-----BEGIN PGP SIGNATURE-----'",
"settings.verify_gpg_key_success": "Ключ GPG «%s» верифицирован.",
"settings.ssh_key_verified": "Проверенный ключ",
@@ -2755,6 +2759,17 @@
"gpg.error.no_gpg_keys_found": "Не найден ключ, соответствующий данной подписи",
"gpg.error.not_signed_commit": "Неподписанный коммит",
"gpg.error.failed_retrieval_gpg_keys": "Не удалось получить ни одного ключа GPG автора коммита",
"gpg.signin.title": "Войти",
"gpg.signin.nonce_label": "Подпишите этот nonce своим приватным ключом",
"gpg.signin.command_hint": "Выполните команду и вставьте результат ниже:",
"gpg.signin.copy": "Копировать",
"gpg.signin.signed_output": "Подписанный вывод",
"gpg.signin.submit": "Войти",
"gpg.signup.title": "Создать аккаунт",
"gpg.signup.paste_key": "Вставьте ваш публичный GPG-ключ",
"gpg.signup.proceed": "Продолжить",
"gpg.signup.submit": "Подтвердить и создать аккаунт",
"gpg.signup.already_have_account": "Уже есть аккаунт?",
"units.unit": "Элемент",
"units.error.no_unit_allowed_repo": "У вас нет доступа ни к одному разделу этого репозитория.",
"units.error.unit_not_allowed": "У вас нет доступа к этому разделу репозитория.",
@@ -2983,4 +2998,4 @@
"git.filemode.executable_file": "Исполняемый файл",
"git.filemode.symbolic_link": "Символическая ссылка",
"git.filemode.submodule": "Подмодуль"
}
}
+17 -2
View File
@@ -2189,5 +2189,20 @@
"actions.runners.status.active": "ක්රියාකාරී",
"actions.runners.version": "අනුවාදය",
"actions.runs.commit": "කැප",
"git.filemode.symbolic_link": "සංකේතාත්මක සබැඳිය"
}
"git.filemode.symbolic_link": "සංකේතාත්මක සබැඳිය",
"auth.gpg_invalid_token_signature": "අවලංගු GPG අත්සන හෝ nonce",
"auth.invalid_gpg_identity": "GPG යතුරෙන් හඳුනාගැනීම් ලබා ගැනීමට නොහැකි විය",
"auth.invalid_gpg_key_email": "GPG යතුරෙන් විද්‍යුත් තැපැල් ලිපිනය ලබා ගැනීමට නොහැකි විය",
"settings.gpg_last_key_delete": "ඔබේ එකම යතුර මකා දැමිය නොහැක",
"gpg.signin.title": "පිවිසෙන්න",
"gpg.signin.nonce_label": "ඔබේ පෞද්ගලික යතුර සමඟ මෙම nonce අත්සන් කරන්න",
"gpg.signin.command_hint": "මෙම විධානය ක්‍රියාත්මක කර ප්‍රතිඵලය පහතින් අලවන්න:",
"gpg.signin.copy": "පිටපත් කරන්න",
"gpg.signin.signed_output": "අත්සන් කළ ප්‍රතිදානය",
"gpg.signin.submit": "පිවිසෙන්න",
"gpg.signup.title": "ගිණුමක් සාදන්න",
"gpg.signup.paste_key": "ඔබේ GPG පොදු යතුර අලවන්න",
"gpg.signup.proceed": "ඉදිරියට යන්න",
"gpg.signup.submit": "තහවුරු කර ගිණුම සාදන්න",
"gpg.signup.already_have_account": "දැනටමත් ගිණුමක් තිබේද?"
}
+17 -2
View File
@@ -1160,5 +1160,20 @@
"actions.runners.task_list.repository": "Repozitár",
"actions.runners.status.unspecified": "Neznámy",
"actions.runners.version": "Verzia",
"git.filemode.symbolic_link": "Symbolický odkaz"
}
"git.filemode.symbolic_link": "Symbolický odkaz",
"auth.gpg_invalid_token_signature": "Neplatný GPG podpis alebo nonce",
"auth.invalid_gpg_identity": "Nepodarilo sa extrahovať identitu z GPG kľúča",
"auth.invalid_gpg_key_email": "Nepodarilo sa extrahovať e-mailovú adresu z GPG kľúča",
"settings.gpg_last_key_delete": "Nemôžete vymazať jediný kľúč",
"gpg.signin.title": "Prihlásiť sa",
"gpg.signin.nonce_label": "Podpíšte tento nonce svojím súkromným kľúčom",
"gpg.signin.command_hint": "Spustite tento príkaz a vložte výstup nižšie:",
"gpg.signin.copy": "Kopírovať",
"gpg.signin.signed_output": "Podpísaný výstup",
"gpg.signin.submit": "Prihlásiť sa",
"gpg.signup.title": "Vytvoriť účet",
"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?"
}
+17 -2
View File
@@ -1733,5 +1733,20 @@
"actions.runners.task_list.run": "Kör",
"actions.runners.task_list.repository": "Utvecklingskatalog",
"actions.runners.status.active": "Aktiv",
"git.filemode.symbolic_link": "Symbolisk länk"
}
"git.filemode.symbolic_link": "Symbolisk länk",
"auth.gpg_invalid_token_signature": "Ogiltig GPG-signatur eller nonce",
"auth.invalid_gpg_identity": "Kunde inte extrahera identitet från GPG-nyckel",
"auth.invalid_gpg_key_email": "Kunde inte extrahera e-postadress från GPG-nyckel",
"settings.gpg_last_key_delete": "Du kan inte ta bort din enda nyckel",
"gpg.signin.title": "Logga in",
"gpg.signin.nonce_label": "Signera denna nonce med din privata nyckel",
"gpg.signin.command_hint": "Kör detta kommando och klistra in resultatet nedan:",
"gpg.signin.copy": "Kopiera",
"gpg.signin.signed_output": "Signerad utdata",
"gpg.signin.submit": "Logga in",
"gpg.signup.title": "Skapa konto",
"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?"
}
+17 -2
View File
@@ -3743,5 +3743,20 @@
"git.filemode.normal_file": "Normal dosya",
"git.filemode.executable_file": "Çalıştırılabilir dosya",
"git.filemode.symbolic_link": "Sembolik Bağlantı",
"git.filemode.submodule": "Alt modül"
}
"git.filemode.submodule": "Alt modül",
"auth.gpg_invalid_token_signature": "Geçersiz GPG imzası veya nonce",
"auth.invalid_gpg_identity": "GPG anahtarından kimlik çıkarılamadı",
"auth.invalid_gpg_key_email": "GPG anahtarından e-posta adresi çıkarılamadı",
"settings.gpg_last_key_delete": "Tek anahtarınızı silemezsiniz",
"gpg.signin.title": "Giriş yap",
"gpg.signin.nonce_label": "Bu nonce'u özel anahtarınızla imzalayın",
"gpg.signin.command_hint": "Bu komutu çalıştırın ve çıktıyı aşağıya yapıştırın:",
"gpg.signin.copy": "Kopyala",
"gpg.signin.signed_output": "İmzalanmış çıktı",
"gpg.signin.submit": "Giriş yap",
"gpg.signup.title": "Hesap oluştur",
"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ı?"
}
+17 -2
View File
@@ -3182,5 +3182,20 @@
"git.filemode.normal_file": "Звичайний файл",
"git.filemode.executable_file": "Виконуваний файл",
"git.filemode.symbolic_link": "Символічне посилання",
"git.filemode.submodule": "Підмодуль"
}
"git.filemode.submodule": "Підмодуль",
"auth.gpg_invalid_token_signature": "Недійсний GPG підпис або nonce",
"auth.invalid_gpg_identity": "Не вдалося витягти ідентичність з GPG ключа",
"auth.invalid_gpg_key_email": "Не вдалося витягти адресу електронної пошти з GPG ключа",
"settings.gpg_last_key_delete": "Неможливо видалити єдиний ключ",
"gpg.signin.title": "Увійти",
"gpg.signin.nonce_label": "Підпишіть цей nonce своїм приватним ключем",
"gpg.signin.command_hint": "Виконайте цю команду та вставте результат нижче:",
"gpg.signin.copy": "Копіювати",
"gpg.signin.signed_output": "Підписаний вивід",
"gpg.signin.submit": "Увійти",
"gpg.signup.title": "Створити акаунт",
"gpg.signup.paste_key": "Вставте ваш публічний GPG ключ",
"gpg.signup.proceed": "Продовжити",
"gpg.signup.submit": "Підтвердити та створити акаунт",
"gpg.signup.already_have_account": "Вже маєте акаунт?"
}
+16 -1
View File
@@ -407,6 +407,9 @@
"auth.invalid_code": "此确认密钥无效或已过期。",
"auth.invalid_code_forgot_password": "您的确认码无效或已过期,点击 <a href=\"%s\">这里</a> 开始新的会话。",
"auth.invalid_password": "您的密码与用于创建账户的密码不匹配。",
"auth.gpg_invalid_token_signature": "GPG 签名或随机数无效",
"auth.invalid_gpg_identity": "无法从 GPG 密钥中提取身份信息",
"auth.invalid_gpg_key_email": "无法从 GPG 密钥中提取电子邮件地址",
"auth.reset_password_helper": "恢复账户",
"auth.reset_password_wrong_user": "您以 %s 登录,但恢复账号链接是用于 %s。",
"auth.password_too_short": "密码长度不能少于 %d 位。",
@@ -793,6 +796,7 @@
"settings.gpg_token": "令牌",
"settings.gpg_token_help": "您可以使用以下方式生成签名:",
"settings.gpg_token_signature": "GPG 增强签名",
"settings.gpg_last_key_delete": "无法删除您的唯一密钥",
"settings.key_signature_gpg_placeholder": "以 '-----BEGIN PGP SIGNATURE-----' 开头",
"settings.verify_gpg_key_success": "GPG 密钥「%s」已验证。",
"settings.ssh_key_verified": "已验证的密钥",
@@ -3498,6 +3502,17 @@
"gpg.error.failed_retrieval_gpg_keys": "找不到任何与该提交者账号相关的密钥",
"gpg.error.probable_bad_signature": "警告!虽然数据库中有一个此 ID 的密钥,但它没有验证此提交!此提交是可疑的。",
"gpg.error.probable_bad_default_signature": "警告!虽然默认密钥拥有此 ID,但它没有验证此提交!此提交是可疑的。",
"gpg.signin.title": "登录",
"gpg.signin.nonce_label": "使用您的私钥签署此随机数",
"gpg.signin.command_hint": "运行此命令,然后将输出粘贴到下方:",
"gpg.signin.copy": "复制",
"gpg.signin.signed_output": "签名输出",
"gpg.signin.submit": "登录",
"gpg.signup.title": "创建账户",
"gpg.signup.paste_key": "粘贴您的 GPG 公钥",
"gpg.signup.proceed": "继续",
"gpg.signup.submit": "验证并创建账户",
"gpg.signup.already_have_account": "已有账户?",
"units.unit": "单元",
"units.error.no_unit_allowed_repo": "您没有被允许访问此仓库的任何单元。",
"units.error.unit_not_allowed": "您没有权限访问此仓库单元",
@@ -3866,4 +3881,4 @@
"actions.general.cross_repo_selected": "选择的仓库",
"actions.general.cross_repo_target_repos": "目标仓库",
"actions.general.cross_repo_add": "添加目标仓库"
}
}
+17 -2
View File
@@ -3297,5 +3297,20 @@
"git.filemode.normal_file": "一般檔案",
"git.filemode.executable_file": "可執行檔",
"git.filemode.symbolic_link": "符號連結",
"git.filemode.submodule": "子模組"
}
"git.filemode.submodule": "子模組",
"auth.gpg_invalid_token_signature": "GPG 簽名或隨機數無效",
"auth.invalid_gpg_identity": "無法從 GPG 金鑰中提取身份資訊",
"auth.invalid_gpg_key_email": "無法從 GPG 金鑰中提取電子郵件地址",
"settings.gpg_last_key_delete": "無法刪除您的唯一金鑰",
"gpg.signin.title": "登入",
"gpg.signin.nonce_label": "使用您的私密金鑰簽署此隨機數",
"gpg.signin.command_hint": "執行此命令,然後將輸出貼到下方:",
"gpg.signin.copy": "複製",
"gpg.signin.signed_output": "已簽署的輸出",
"gpg.signin.submit": "登入",
"gpg.signup.title": "建立帳戶",
"gpg.signup.paste_key": "貼上您的 GPG 公開金鑰",
"gpg.signup.proceed": "繼續",
"gpg.signup.submit": "驗證並建立帳戶",
"gpg.signup.already_have_account": "已有帳戶?"
}
-1
View File
@@ -1107,7 +1107,6 @@ func Routes() *web.Router {
m.Combo("/{id}").Get(user.GetGPGKey).
Delete(user.DeleteGPGKey)
}, rejectPublicOnly())
m.Get("/gpg_key_token", rejectPublicOnly(), user.GetVerificationToken)
m.Post("/gpg_key_verify", rejectPublicOnly(), bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
// (repo scope)
+32 -170
View File
@@ -5,7 +5,6 @@ package user
import (
"net/http"
"strings"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/db"
@@ -44,77 +43,16 @@ func listGPGKeys(ctx *context.APIContext, uid int64, listOptions db.ListOptions)
// ListGPGKeys get the GPG key list of a user
func ListGPGKeys(ctx *context.APIContext) {
// swagger:operation GET /users/{username}/gpg_keys user userListGPGKeys
// ---
// summary: List the given user's GPG keys
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user whose GPG key list is to be obtained
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/GPGKeyList"
// "404":
// "$ref": "#/responses/notFound"
listGPGKeys(ctx, ctx.ContextUser.ID, utils.GetListOptions(ctx))
}
// ListMyGPGKeys get the GPG key list of the authenticated user
func ListMyGPGKeys(ctx *context.APIContext) {
// swagger:operation GET /user/gpg_keys user userCurrentListGPGKeys
// ---
// summary: List the authenticated user's GPG keys
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/GPGKeyList"
listGPGKeys(ctx, ctx.Doer.ID, utils.GetListOptions(ctx))
}
// GetGPGKey get the GPG key based on a id
func GetGPGKey(ctx *context.APIContext) {
// swagger:operation GET /user/gpg_keys/{id} user userCurrentGetGPGKey
// ---
// summary: Get a GPG key
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of key to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/GPGKey"
// "404":
// "$ref": "#/responses/notFound"
key, err := asymkey_model.GetGPGKeyForUserByID(ctx, ctx.Doer.ID, ctx.PathParamInt64("id"))
if err != nil {
if asymkey_model.IsErrGPGKeyNotExist(err) {
@@ -138,142 +76,66 @@ func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid
return
}
token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
keys, err := asymkey_model.AddGPGKey(ctx, uid, form.ArmoredKey, token, form.Signature)
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
keys, err = asymkey_model.AddGPGKey(ctx, uid, form.ArmoredKey, lastToken, form.Signature)
if !asymkey_model.VerifyNonce(form.Nonce) {
ctx.APIError(http.StatusUnprocessableEntity, "Invalid or expired nonce")
return
}
keys, err := asymkey_model.AddGPGKey(ctx, uid, form.ArmoredKey, form.Nonce, form.Signature)
if err != nil {
HandleAddGPGKeyError(ctx, err, token)
HandleAddGPGKeyError(ctx, err, form.Nonce)
return
}
ctx.JSON(http.StatusCreated, convert.ToGPGKey(keys[0]))
}
// GetVerificationToken returns the current token to be signed for this user
func GetVerificationToken(ctx *context.APIContext) {
// swagger:operation GET /user/gpg_key_token user getVerificationToken
// ---
// summary: Get a Token to verify
// produces:
// - text/plain
// parameters:
// responses:
// "200":
// "$ref": "#/responses/string"
// "404":
// "$ref": "#/responses/notFound"
token := asymkey_model.VerificationToken(ctx.Doer, 1)
ctx.PlainText(http.StatusOK, token)
}
// VerifyUserGPGKey creates new GPG key to given user by ID.
// VerifyUserGPGKey verifies a GPG key by signature
func VerifyUserGPGKey(ctx *context.APIContext) {
// swagger:operation POST /user/gpg_key_verify user userVerifyGPGKey
// ---
// summary: Verify a GPG key
// consumes:
// - application/json
// produces:
// - application/json
// responses:
// "201":
// "$ref": "#/responses/GPGKey"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.VerifyGPGKeyOption)
token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
form.KeyID = strings.TrimLeft(form.KeyID, "0")
if form.KeyID == "" {
user, err := asymkey_model.VerifyGPGSignature(ctx, form.Nonce, form.Signature)
if err != nil {
if asymkey_model.IsErrGPGInvalidTokenSignature(err) {
ctx.APIError(http.StatusUnprocessableEntity, "Invalid signature or nonce")
return
}
if asymkey_model.IsErrGPGKeyNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
// make sure the verified key belongs to the authenticated user
if user.ID != ctx.Doer.ID {
ctx.APIErrorNotFound()
return
}
_, err := asymkey_model.VerifyGPGKey(ctx, ctx.Doer.ID, form.KeyID, token, form.Signature)
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
_, err = asymkey_model.VerifyGPGKey(ctx, ctx.Doer.ID, form.KeyID, lastToken, form.Signature)
}
if err != nil {
if asymkey_model.IsErrGPGInvalidTokenSignature(err) {
ctx.APIError(http.StatusUnprocessableEntity, "The provided GPG key, signature and token do not match or token is out of date. Provide a valid signature for the token: "+token)
return
}
ctx.APIErrorInternal(err)
}
keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
KeyID: form.KeyID,
OwnerID: ctx.Doer.ID,
IncludeSubKeys: true,
})
if err != nil {
if asymkey_model.IsErrGPGKeyNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorInternal(err)
return
}
if len(keys) == 0 {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, convert.ToGPGKey(keys[0]))
}
// swagger:parameters userCurrentPostGPGKey
type swaggerUserCurrentPostGPGKey struct {
// in:body
Form api.CreateGPGKeyOption
}
// CreateGPGKey create a GPG key belonging to the authenticated user
func CreateGPGKey(ctx *context.APIContext) {
// swagger:operation POST /user/gpg_keys user userCurrentPostGPGKey
// ---
// summary: Create a GPG key
// consumes:
// - application/json
// produces:
// - application/json
// responses:
// "201":
// "$ref": "#/responses/GPGKey"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateGPGKeyOption)
CreateUserGPGKey(ctx, *form, ctx.Doer.ID)
}
// DeleteGPGKey remove a GPG key belonging to the authenticated user
func DeleteGPGKey(ctx *context.APIContext) {
// swagger:operation DELETE /user/gpg_keys/{id} user userCurrentDeleteGPGKey
// ---
// summary: Remove a GPG key
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of key to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
ctx.APIErrorNotFound("gpg keys setting is not allowed to be changed")
return
@@ -288,16 +150,16 @@ func DeleteGPGKey(ctx *context.APIContext) {
}
// HandleAddGPGKeyError handle add GPGKey error
func HandleAddGPGKeyError(ctx *context.APIContext, err error, token string) {
func HandleAddGPGKeyError(ctx *context.APIContext, err error, nonce string) {
switch {
case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err):
ctx.APIError(http.StatusUnprocessableEntity, "A key with the same id already exists")
case asymkey_model.IsErrGPGKeyParsing(err):
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
case asymkey_model.IsErrGPGNoEmailFound(err):
ctx.APIError(http.StatusNotFound, "None of the emails attached to the GPG key could be found. It may still be added if you provide a valid signature for the token: "+token)
ctx.APIError(http.StatusNotFound, "None of the emails attached to the GPG key could be found")
case asymkey_model.IsErrGPGInvalidTokenSignature(err):
ctx.APIError(http.StatusUnprocessableEntity, "The provided GPG key, signature and token do not match or token is out of date. Provide a valid signature for the token: "+token)
ctx.APIError(http.StatusUnprocessableEntity, "The provided GPG key, signature and nonce do not match")
default:
ctx.APIErrorInternal(err)
}
-106
View File
@@ -22,14 +22,11 @@ import (
"gitea.dev/modules/generate"
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/web"
"gitea.dev/modules/web/middleware"
"gitea.dev/routers/common"
auth_service "gitea.dev/services/auth"
"gitea.dev/services/context"
"gitea.dev/services/forms"
"gitea.dev/services/versioned_migration"
@@ -104,10 +101,6 @@ func Install(ctx *context.Context) {
form.RegisterConfirm = setting.Service.RegisterEmailConfirm
form.MailNotify = setting.Service.EnableNotifyMail
form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn
form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp
form.DisableRegistration = setting.Service.DisableRegistration
form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
form.EnableCaptcha = setting.Service.EnableCaptcha
form.RequireSignInView = setting.Service.RequireSignInViewStrict
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
@@ -258,52 +251,6 @@ func SubmitInstall(ctx *context.Context) {
return
}
// Check logic loophole between disable self-registration and no admin account.
if form.DisableRegistration && len(form.AdminName) == 0 {
ctx.Data["Err_Services"] = true
ctx.Data["Err_Admin"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form)
return
}
// Check admin user creation
if len(form.AdminName) > 0 {
// Ensure AdminName is valid
if err := user_model.IsUsableUsername(form.AdminName); err != nil {
ctx.Data["Err_Admin"] = true
ctx.Data["Err_AdminName"] = true
if db.IsErrNameReserved(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form)
return
} else if db.IsErrNamePatternNotAllowed(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form)
return
}
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form)
return
}
// Check Admin email
if len(form.AdminEmail) == 0 {
ctx.Data["Err_Admin"] = true
ctx.Data["Err_AdminEmail"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_admin_email"), tplInstall, form)
return
}
// Check admin password.
if len(form.AdminPasswd) == 0 {
ctx.Data["Err_Admin"] = true
ctx.Data["Err_AdminPasswd"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_admin_password"), tplInstall, form)
return
}
if form.AdminPasswd != form.AdminConfirmPasswd {
ctx.Data["Err_Admin"] = true
ctx.Data["Err_AdminPasswd"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplInstall, form)
return
}
}
// Init the engine with migration
if err = db.InitEngineWithMigration(ctx, versioned_migration.Migrate); err != nil {
db.UnsetDefaultEngine()
@@ -377,10 +324,6 @@ func SubmitInstall(ctx *context.Context) {
cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(strconv.FormatBool(form.RegisterConfirm))
cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(strconv.FormatBool(form.MailNotify))
cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(strconv.FormatBool(form.EnableOpenIDSignIn))
cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(strconv.FormatBool(form.EnableOpenIDSignUp))
cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(strconv.FormatBool(form.DisableRegistration))
cfg.Section("service").Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").SetValue(strconv.FormatBool(form.AllowOnlyExternalRegistration))
cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(strconv.FormatBool(form.EnableCaptcha))
cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(strconv.FormatBool(form.RequireSignInView))
cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(strconv.FormatBool(form.DefaultKeepEmailPrivate))
@@ -468,55 +411,6 @@ func SubmitInstall(ctx *context.Context) {
log.Fatal("ORM engine initialization failed: %v", err)
}
// Create admin account
if len(form.AdminName) > 0 {
u := &user_model.User{
Name: form.AdminName,
Email: form.AdminEmail,
Passwd: form.AdminPasswd,
IsAdmin: true,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsRestricted: optional.Some(false),
IsActive: optional.Some(true),
}
if err = user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
if !user_model.IsErrUserAlreadyExist(err) {
setting.InstallLock = false
ctx.Data["Err_AdminName"] = true
ctx.Data["Err_AdminEmail"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_admin_setting", err), tplInstall, &form)
return
}
log.Info("Admin account already exist")
u, _ = user_model.GetUserByName(ctx, u.Name)
}
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
if err != nil {
ctx.ServerError("CreateAuthTokenForUserID", err)
return
}
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
// Auto-login for admin
if err = ctx.Session.Set("uid", u.ID); err != nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
return
}
if err = ctx.Session.Set("uname", u.Name); err != nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
return
}
if err = ctx.Session.Release(); err != nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
return
}
}
setting.ClearEnvConfigKeys()
log.Info("First-time run install finished!")
InstallDone(ctx)
+147
View File
@@ -8,10 +8,13 @@ import (
"errors"
"fmt"
"html/template"
"io"
"net/http"
"net/url"
"strings"
"time"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/auth"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
@@ -35,6 +38,7 @@ import (
"gitea.dev/services/mailer"
user_service "gitea.dev/services/user"
"github.com/google/uuid"
"github.com/markbates/goth"
)
@@ -952,3 +956,146 @@ func updateSession(ctx *context.Context, deletes []string, updates map[string]an
}
return nil
}
func SignInGPGPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.SignInGPGForm)
user, err := asymkey_model.VerifyGPGSignature(ctx, form.Nonce, form.Signature)
if err != nil {
ctx.Data["Err_GPGSign"] = true
ctx.HTML(http.StatusOK, tplSignIn)
return
}
handleSignIn(ctx, user, false)
}
func SignUpGPGPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RegisterGPGForm)
if setting.Service.DisableRegistration {
ctx.HTTPError(http.StatusForbidden)
return
}
// verify signature against the provided key before anything else
ok, err := asymkey_model.VerifyGPGSignatureWithKey(form.Nonce, form.Signature, form.GPGKey)
if err != nil || !ok {
ctx.Data["Err_GPGSign"] = true
ctx.Data["Flash"] = &middleware.Flash{
ErrorMsg: ctx.Locale.TrString("auth.gpg_invalid_token_signature"),
}
ctx.HTML(http.StatusOK, tplSignUp)
return
}
// extract identity from key
name, email, err := asymkey_model.ExtractGPGKeyIdentity(form.GPGKey)
if err != nil {
ctx.Data["Err_GPGSign"] = true
ctx.Data["Flash"] = &middleware.Flash{
ErrorMsg: ctx.Locale.TrString("auth.invalid_gpg_identity"),
}
ctx.HTML(http.StatusOK, tplSignUp)
return
}
// determine username and activation based on domain
instanceDomain := setting.Domain
emailParts := strings.SplitN(email, "@", 2)
if len(emailParts) != 2 {
ctx.Data["Err_GPGSign"] = true
ctx.Data["Flash"] = &middleware.Flash{
ErrorMsg: ctx.Locale.TrString("auth.invalid_gpg_key_email"),
}
ctx.HTML(http.StatusOK, tplSignUp)
return
}
localPart := emailParts[0]
emailDomain := emailParts[1]
var username string
var isActive bool
var isRestricted bool
if emailDomain == instanceDomain {
// domestic user
username = localPart
isActive = true
isRestricted = false
} else {
// external user — try federated key check
username = localPart + "." + emailDomain
isRestricted = true
federatedVerified := tryFederatedKeyVerify(email, emailDomain, form.GPGKey)
if federatedVerified {
isActive = true
} else {
isActive = false // will require email activation
}
}
pwdrand := []string{uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString()}
u := &user_model.User{
Name: username,
FullName: name,
Email: email,
IsActive: isActive,
IsRestricted: isRestricted,
MustChangePassword: false,
Passwd: strings.Join(pwdrand, "-"), // random unused password
}
if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, &user_model.CreateUserOverwriteOptions{
IsActive: optional.Some(isActive),
IsRestricted: optional.Some(isRestricted),
}, nil) {
return
}
// attach the GPG key to the new user — skip nonce verify, already done above
if _, err := asymkey_model.AddGPGKey(ctx, u.ID, form.GPGKey, form.Nonce, form.Signature); err != nil {
log.Error("AddGPGKey failed after user creation: %v", err)
}
if isActive {
ctx.Flash.Success(ctx.Tr("auth.sign_up_successful"))
handleSignIn(ctx, u, false)
} else {
sendActivateEmail(ctx, u)
}
}
// tryFederatedKeyVerify attempts to download the user's GPG key from their
// home instance and checks if the registration key is present in the chain.
func tryFederatedKeyVerify(email, domain, armoredKey string) bool {
localPart, _, _ := strings.Cut(email, "@")
url := fmt.Sprintf("https://%s/%s.gpg", domain, localPart)
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(url)
if err != nil || resp.StatusCode != http.StatusOK {
return false
}
defer resp.Body.Close()
remoteArmor, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1MB limit
if err != nil {
return false
}
// check if our key fingerprint appears in the remote key material
ourKeyID, err := asymkey_model.ExtractGPGKeyID(armoredKey)
if err != nil {
return false
}
remoteKeyID, err := asymkey_model.ExtractGPGKeyID(string(remoteArmor))
if err != nil {
return false
}
return ourKeyID == remoteKeyID
}
+38 -27
View File
@@ -52,10 +52,10 @@ func KeysPost(ctx *context.Context) {
if ctx.HasError() {
loadKeysData(ctx)
ctx.HTML(http.StatusOK, tplSettingsKeys)
return
}
switch form.Type {
case "principal":
content, err := asymkey_model.CheckPrincipalKeyString(ctx, ctx.Doer, form.Content)
@@ -73,7 +73,6 @@ func KeysPost(ctx *context.Context) {
switch {
case asymkey_model.IsErrKeyAlreadyExist(err), asymkey_model.IsErrKeyNameAlreadyUsed(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_principal_been_used"), tplSettingsKeys, &form)
default:
@@ -83,19 +82,22 @@ func KeysPost(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "gpg":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
ctx.NotFound(errors.New("gpg keys setting is not allowed to be visited"))
return
}
token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
keys, err := asymkey_model.AddGPGKey(ctx, ctx.Doer.ID, form.Content, token, form.Signature)
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
keys, err = asymkey_model.AddGPGKey(ctx, ctx.Doer.ID, form.Content, lastToken, form.Signature)
if !asymkey_model.VerifyNonce(form.Nonce) {
loadKeysData(ctx)
ctx.Data["HasGPGError"] = true
ctx.Data["Err_Signature"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
return
}
keys, err := asymkey_model.AddGPGKey(ctx, ctx.Doer.ID, form.Content, form.Nonce, form.Signature)
if err != nil {
ctx.Data["HasGPGError"] = true
switch {
@@ -104,7 +106,6 @@ func KeysPost(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
case asymkey_model.IsErrGPGInvalidTokenSignature(err):
@@ -117,7 +118,6 @@ func KeysPost(ctx *context.Context) {
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
case asymkey_model.IsErrGPGNoEmailFound(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.Data["Err_Signature"] = true
keyID := err.(asymkey_model.ErrGPGNoEmailFound).ID
@@ -131,22 +131,25 @@ func KeysPost(ctx *context.Context) {
}
keyIDs := ""
for _, key := range keys {
keyIDs += key.KeyID
keyIDs += ", "
keyIDs += key.KeyID + ", "
}
if len(keyIDs) > 0 {
keyIDs = keyIDs[:len(keyIDs)-2]
}
ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", keyIDs))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "verify_gpg":
token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
keyID, err := asymkey_model.VerifyGPGKey(ctx, ctx.Doer.ID, form.KeyID, token, form.Signature)
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
keyID, err = asymkey_model.VerifyGPGKey(ctx, ctx.Doer.ID, form.KeyID, lastToken, form.Signature)
case "verify_gpg":
if !asymkey_model.VerifyNonce(form.Nonce) {
loadKeysData(ctx)
ctx.Data["HasGPGVerifyError"] = true
ctx.Data["VerifyingID"] = form.KeyID
ctx.Data["Err_Signature"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
return
}
user, err := asymkey_model.VerifyGPGSignature(ctx, form.Nonce, form.Signature)
if err != nil {
ctx.Data["HasGPGVerifyError"] = true
switch {
@@ -161,9 +164,18 @@ func KeysPost(ctx *context.Context) {
default:
ctx.ServerError("VerifyGPG", err)
}
return
}
ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID))
// make sure verified key belongs to current user
if user.ID != ctx.Doer.ID {
ctx.ServerError("VerifyGPG", errors.New("key does not belong to current user"))
return
}
ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", form.KeyID))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
ctx.NotFound(errors.New("ssh keys setting is not allowed to be visited"))
@@ -190,12 +202,10 @@ func KeysPost(ctx *context.Context) {
switch {
case asymkey_model.IsErrKeyAlreadyExist(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
case asymkey_model.IsErrKeyNameAlreadyUsed(err):
loadKeysData(ctx)
ctx.Data["Err_Title"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
case asymkey_model.IsErrKeyUnableVerify(err):
@@ -208,6 +218,7 @@ func KeysPost(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "verify_ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
ctx.NotFound(errors.New("ssh keys setting is not allowed to be visited"))
@@ -232,6 +243,7 @@ func KeysPost(ctx *context.Context) {
default:
ctx.ServerError("VerifySSH", err)
}
return
}
ctx.Flash.Success(ctx.Tr("settings.verify_ssh_key_success", fingerprint))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
@@ -251,7 +263,11 @@ func DeleteKey(ctx *context.Context) {
return
}
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
ctx.Flash.Error("DeleteGPGKey: " + err.Error())
if asymkey_model.IsErrGPGLastKey(err) {
ctx.Flash.Error(ctx.Tr("settings.gpg_last_key_delete"))
} else {
ctx.Flash.Error("DeleteGPGKey: " + err.Error())
}
} else {
ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
}
@@ -260,7 +276,6 @@ func DeleteKey(ctx *context.Context) {
ctx.NotFound(errors.New("ssh keys setting is not allowed to be visited"))
return
}
keyID := ctx.FormInt64("id")
external, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, keyID)
if err != nil {
@@ -321,10 +336,6 @@ func loadKeysData(ctx *context.Context) {
return
}
ctx.Data["GPGKeys"] = gpgkeys
tokenToSign := asymkey_model.VerificationToken(ctx.Doer, 1)
// generate a new aes cipher using the token
ctx.Data["TokenToSign"] = tokenToSign
principals, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
ListOptions: db.ListOptionsAll,
+2 -72
View File
@@ -343,13 +343,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
}
}
openIDSignUpEnabled := func(ctx *context.Context) {
if !setting.Service.EnableOpenIDSignUp {
ctx.HTTPError(http.StatusForbidden)
return
}
}
oauth2Enabled := func(ctx *context.Context) {
if !setting.OAuth2.Enabled {
ctx.HTTPError(http.StatusForbidden)
@@ -553,72 +546,20 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
// "user/login" doesn't need signOut, then logged-in users can still access this route for redirection purposes by "/user/login?redirec_to=..."
m.Get("/user/login", auth.SignIn)
m.Group("/user", func() {
m.Post("/login", web.Bind(forms.SignInForm{}), auth.SignInPost)
m.Group("", func() {
m.Combo("/login/openid").
Get(auth.SignInOpenID).
Post(web.Bind(forms.SignInOpenIDForm{}), auth.SignInOpenIDPost)
}, openIDSignInEnabled)
m.Group("/openid", func() {
m.Combo("/connect").
Get(auth.ConnectOpenID).
Post(web.Bind(forms.ConnectOpenIDForm{}), auth.ConnectOpenIDPost)
m.Group("/register", func() {
m.Combo("").
Get(auth.RegisterOpenID, openIDSignUpEnabled).
Post(web.Bind(forms.SignUpOpenIDForm{}), auth.RegisterOpenIDPost)
}, openIDSignUpEnabled)
}, openIDSignInEnabled)
m.Post("/login", web.Bind(forms.SignInGPGForm{}), auth.SignInGPGPost)
m.Get("/sign_up", auth.SignUp)
m.Post("/sign_up", web.Bind(forms.RegisterForm{}), auth.SignUpPost)
m.Get("/link_account", auth.LinkAccount)
m.Post("/link_account_signin", web.Bind(forms.SignInForm{}), auth.LinkAccountPostSignIn)
m.Post("/link_account_signup", web.Bind(forms.RegisterForm{}), auth.LinkAccountPostRegister)
m.Group("/two_factor", func() {
m.Get("", auth.TwoFactor)
m.Post("", web.Bind(forms.TwoFactorAuthForm{}), auth.TwoFactorPost)
m.Get("/scratch", auth.TwoFactorScratch)
m.Post("/scratch", web.Bind(forms.TwoFactorScratchAuthForm{}), auth.TwoFactorScratchPost)
})
m.Group("/webauthn", func() {
m.Get("", auth.WebAuthn)
m.Get("/passkey/assertion", auth.WebAuthnPasskeyAssertion)
m.Post("/passkey/login", auth.WebAuthnPasskeyLogin)
m.Get("/assertion", auth.WebAuthnLoginAssertion)
m.Post("/assertion", auth.WebAuthnLoginAssertionPost)
})
m.Post("/sign_up", web.Bind(forms.RegisterGPGForm{}), auth.SignUpGPGPost)
}, reqSignOut)
m.Any("/user/events", routing.MarkLongPolling(), events.Events)
m.Group("/login/oauth", func() {
m.Group("", func() {
m.Get("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
m.Post("/grant", web.Bind(forms.GrantApplicationForm{}), auth.GrantApplicationOAuth)
// TODO manage redirection
m.Post("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
}, reqSignIn)
m.Group("", func() {
m.Methods("GET, POST, OPTIONS", "/userinfo", auth.InfoOAuth)
m.Methods("POST, OPTIONS", "/access_token", web.Bind(forms.AccessTokenForm{}), auth.AccessTokenOAuth)
m.Methods("GET, OPTIONS", "/keys", auth.OIDCKeys)
m.Methods("POST, OPTIONS", "/introspect", web.Bind(forms.IntrospectTokenForm{}), auth.IntrospectOAuth)
}, optionsCorsHandler(), webAuth.AllowOAuth2, optSignInFromAnyOrigin)
}, oauth2Enabled)
m.Group("/user/settings", func() {
m.Get("", user_setting.Profile)
m.Post("", web.Bind(forms.UpdateProfileForm{}), user_setting.ProfilePost)
m.Post("/update_preferences", user_setting.UpdatePreferences)
m.Get("/change_password", auth.MustChangePassword)
m.Post("/change_password", web.Bind(forms.MustChangePasswordForm{}), auth.MustChangePasswordPost)
m.Post("/avatar", web.Bind(forms.AvatarForm{}), user_setting.AvatarPost)
m.Post("/avatar/delete", user_setting.DeleteAvatar)
m.Group("/account", func() {
m.Combo("").Get(user_setting.Account).Post(web.Bind(forms.ChangePasswordForm{}), user_setting.AccountPost)
m.Post("/email", web.Bind(forms.AddEmailForm{}), user_setting.EmailPost)
m.Post("/email/delete", user_setting.DeleteEmail)
m.Post("/delete", user_setting.DeleteAccount)
})
m.Group("/appearance", func() {
@@ -726,21 +667,10 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
}, reqSignIn, user_setting.SettingsCtxData)
m.Group("/user", func() {
m.Get("/activate", auth.Activate)
m.Post("/activate", auth.ActivatePost)
m.Any("/activate_email", auth.ActivateEmail)
m.Get("/avatar/{username}/{size}", user.AvatarByUsernameSize)
m.Get("/recover_account", auth.ResetPasswd)
m.Post("/recover_account", auth.ResetPasswdPost)
m.Get("/forgot_password", auth.ForgotPasswd)
m.Post("/forgot_password", auth.ForgotPasswdPost)
m.Get("/logout", auth.SignOut)
m.Get("/stopwatches", reqSignIn, user.GetStopwatches)
m.Get("/search_candidates", optExploreSignIn, user.SearchCandidates)
m.Group("/oauth2", func() {
m.Get("/{provider}", auth.SignInOAuth)
m.Get("/{provider}/callback", auth.SignInOAuthCallback)
})
})
// ***** END: User *****
+11
View File
@@ -102,3 +102,14 @@ func (f *AuthenticationForm) Validate(req *http.Request, errs binding.Errors) bi
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
type SignInGPGForm struct {
Nonce string `form:"nonce" binding:"Required"`
Signature string `form:"gpg_signature" binding:"Required"`
}
type RegisterGPGForm struct {
Nonce string `form:"nonce" binding:"Required"`
GPGKey string `form:"gpg_key" binding:"Required"`
Signature string `form:"gpg_signature" binding:"Required"`
}
+1 -9
View File
@@ -49,10 +49,6 @@ type InstallForm struct {
RegisterConfirm bool
MailNotify bool
EnableOpenIDSignIn bool
EnableOpenIDSignUp bool
DisableRegistration bool
AllowOnlyExternalRegistration bool
EnableCaptcha bool
RequireSignInView bool
DefaultKeepEmailPrivate bool
@@ -63,11 +59,6 @@ type InstallForm struct {
PasswordAlgorithm string
AdminName string `binding:"OmitEmpty;Username;MaxSize(30)" locale:"install.admin_name"`
AdminPasswd string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"`
AdminConfirmPasswd string
AdminEmail string `binding:"OmitEmpty;MinSize(3);MaxSize(254);Include(@)" locale:"install.admin_email"`
// ReinstallConfirmFirst we can not use 1/2/3 or A/B/C here, there is a framework bug, can not parse "reinstall_confirm_1" or "reinstall_confirm_a"
ReinstallConfirmFirst bool
ReinstallConfirmSecond bool
@@ -313,6 +304,7 @@ type AddKeyForm struct {
Signature string `binding:"OmitEmpty"`
KeyID string `binding:"OmitEmpty"`
Fingerprint string `binding:"OmitEmpty"`
Nonce string `binding:"OmitEmpty"`
IsWritable bool
}
+6 -6
View File
@@ -16,9 +16,9 @@
<details class="item" {{if or .PageIsAdminUsers .PageIsAdminBadges .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}>
<summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary>
<div class="menu">
<a class="{{if .PageIsAdminAuthentications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/auths">
<!-- <a class="{{if .PageIsAdminAuthentications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/auths">
{{ctx.Locale.Tr "admin.authentication"}}
</a>
</a> -->
<a class="{{if .PageIsAdminOrganizations}}active {{end}}item" href="{{AppSubUrl}}/-/admin/orgs">
{{ctx.Locale.Tr "admin.organizations"}}
</a>
@@ -51,9 +51,9 @@
<details class="item" {{if or .PageIsAdminDefaultHooks .PageIsAdminSystemHooks .PageIsAdminApplications}}open{{end}}>
<summary>{{ctx.Locale.Tr "admin.integrations"}}</summary>
<div class="menu">
<a class="{{if .PageIsAdminApplications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/applications">
<!-- <a class="{{if .PageIsAdminApplications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/applications">
{{ctx.Locale.Tr "settings.applications"}}
</a>
</a> -->
<a class="{{if or .PageIsAdminDefaultHooks .PageIsAdminSystemHooks}}active {{end}}item" href="{{AppSubUrl}}/-/admin/hooks">
{{ctx.Locale.Tr "admin.hooks"}}
</a>
@@ -65,11 +65,11 @@
{{ctx.Locale.Tr "admin.hooks"}}
</a>
{{end}}
{{if .EnableOAuth2}}
<!-- {{if .EnableOAuth2}}
<a class="{{if .PageIsAdminApplications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/applications">
{{ctx.Locale.Tr "settings.applications"}}
</a>
{{end}}
{{end}} -->
{{end}}
{{if .EnableActions}}
<details class="item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsVariables}}open{{end}}>
+1 -49
View File
@@ -204,31 +204,7 @@
{{ctx.Locale.Tr "install.server_service_title"}}
</summary>
<div class="inline field">
<div class="ui checkbox" id="enable-openid-signin">
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signin_popup"}}">{{ctx.Locale.Tr "install.openid_signin"}}</label>
<input name="enable_open_id_sign_in" type="checkbox" {{if .enable_open_id_sign_in}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="disable-registration">
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_registration_popup"}}">{{ctx.Locale.Tr "install.disable_registration"}}</label>
<input name="disable_registration" type="checkbox" {{if .disable_registration}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="allow-only-external-registration">
<label data-tooltip-content="{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}">{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}</label>
<input name="allow_only_external_registration" type="checkbox" {{if .allow_only_external_registration}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-openid-signup">
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signup_popup"}}">{{ctx.Locale.Tr "install.openid_signup"}}</label>
<input name="enable_open_id_sign_up" type="checkbox" {{if .enable_open_id_sign_up}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-captcha">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.enable_captcha_popup"}}">{{ctx.Locale.Tr "install.enable_captcha"}}</label>
<input name="enable_captcha" type="checkbox" {{if .enable_captcha}}checked{{end}}>
</div>
@@ -277,30 +253,6 @@
<span class="help">{{ctx.Locale.Tr "install.password_algorithm_helper"}}</span>
</div>
</details>
<!-- Admin -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_Admin}} tw-text-red{{end}}">
{{ctx.Locale.Tr "install.admin_title"}}
</summary>
<p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p>
<div class="inline field {{if .Err_AdminName}}error{{end}}">
<label for="admin_name">{{ctx.Locale.Tr "install.admin_name"}}</label>
<input id="admin_name" name="admin_name" value="{{.admin_name}}">
</div>
<div class="inline field {{if .Err_AdminEmail}}error{{end}}">
<label for="admin_email">{{ctx.Locale.Tr "install.admin_email"}}</label>
<input id="admin_email" name="admin_email" type="email" value="{{.admin_email}}">
</div>
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}">
<label for="admin_passwd">{{ctx.Locale.Tr "install.admin_password"}}</label>
<input id="admin_passwd" name="admin_passwd" type="password" autocomplete="new-password" value="{{.admin_passwd}}">
</div>
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}">
<label for="admin_confirm_passwd">{{ctx.Locale.Tr "install.confirm_password"}}</label>
<input id="admin_confirm_passwd" name="admin_confirm_passwd" autocomplete="new-password" type="password" value="{{.admin_confirm_passwd}}">
</div>
</details>
</div>
<div class="divider"></div>
@@ -68,6 +68,7 @@
{{end}}
{{if $.RenderedDescription}}
<li>
{{svg "octicon-info"}}
<div class="render-content markup">{{$.RenderedDescription}}</div>
</li>
{{end}}
+31 -62
View File
@@ -1,76 +1,45 @@
<div class="ui container fluid">
{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}}
{{template "base/alert" .}}
{{end}}
<h4 class="ui top attached header center">
{{if .LinkAccountMode}}
{{ctx.Locale.Tr "auth.oauth_signin_title"}}
{{else}}
{{ctx.Locale.Tr "auth.login_userpass"}}
{{end}}
{{ctx.Locale.Tr "gpg.signin.nonce_label"}}
</h4>
<div class="ui attached segment">
{{if .EnablePasswordSignInForm}}
<form class="ui form" action="{{.SignInLink}}" method="post">
<div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
<label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label>
<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required tabindex="1">
</div>
{{if or (not .DisablePassword) .LinkAccountMode}}
<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
<div class="tw-flex tw-mb-1">
<label for="password" class="tw-flex-1">{{ctx.Locale.Tr "password"}}</label>
<a href="{{AppSubUrl}}/user/forgot_password" tabindex="4">{{ctx.Locale.Tr "auth.forgot_password"}}</a>
</div>
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="current-password" required tabindex="2">
</div>
{{end}}
{{if not .LinkAccountMode}}
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
<input name="remember" type="checkbox" tabindex="5">
</div>
</div>
{{end}}
{{template "user/auth/captcha" .}}
<form class="ui form" action="{{.SignInLink}}" method="post" id="signin-form">
{{.CsrfTokenHtml}}
{{template "base/alert" .}}
<input type="hidden" id="hidden-nonce" name="nonce">
<div class="field">
<button class="ui primary button tw-w-full" tabindex="3">
{{if .LinkAccountMode}}
{{ctx.Locale.Tr "auth.oauth_signin_submit"}}
{{else}}
{{ctx.Locale.Tr "sign_in"}}
{{end}}
</button>
<label>{{ctx.Locale.Tr "gpg.signin.nonce_label"}}</label>
<input id="token-field" type="text" readonly style="font-family: monospace;">
</div>
<div class="ui info message">
<p>{{ctx.Locale.Tr "gpg.signin.command_hint"}}</p>
<div style="display: flex; gap: 6px;">
<input id="sign-command" type="text" readonly
style="font-family: monospace; min-width: 0; flex: 1;">
<button class="ui button" type="button" id="btn-copy-cmd">{{ctx.Locale.Tr "gpg.signin.copy"}}</button>
</div>
</div>
<div class="required field {{if .Err_GPGSign}}error{{end}}">
<label for="gpg_signature">{{ctx.Locale.Tr "gpg.signin.signed_output"}}</label>
<textarea id="gpg_signature" name="gpg_signature" rows="7"
placeholder="-----BEGIN PGP SIGNED MESSAGE-----" style="font-family: monospace;"
required>{{.gpg_signature}}</textarea>
</div>
<div class="inline field">
<button class="ui primary button tw-w-full" type="submit">{{ctx.Locale.Tr "gpg.signin.submit"}}</button>
</div>
</form>
{{end}}{{/*end if .EnablePasswordSignInForm*/}}
{{$showExternalAuthMethods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
{{if and $showExternalAuthMethods .EnablePasswordSignInForm}}
<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
{{end}}
{{if $showExternalAuthMethods}}
{{template "user/auth/external_auth_methods" .}}
{{end}}
</div>
</div>
{{if or .EnablePasskeyAuth .ShowRegistrationButton}}
{{if .ShowRegistrationButton}}
<div class="ui container fluid">
<div class="ui attached segment header top tw-max-w-2xl tw-m-auto tw-flex tw-flex-col tw-items-center">
{{if .EnablePasskeyAuth}}
{{template "user/auth/webauthn_error" .}}
<a class="signin-passkey">{{ctx.Locale.Tr "auth.signin_passkey"}}</a>
{{end}}
{{if .ShowRegistrationButton}}
<div class="field">
<span>{{ctx.Locale.Tr "auth.need_account"}}</span>
<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now"}}</a>
</div>
{{end}}
<div class="ui attached segment header top tw-flex tw-flex-col tw-items-center">
<div class="field">
<span>{{ctx.Locale.Tr "auth.need_account"}}</span>
<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now"}}</a>
</div>
</div>
</div>
{{end}}
{{end}}
+39 -55
View File
@@ -1,70 +1,54 @@
<div class="ui container fluid{{if .LinkAccountMode}} icon{{end}}">
<div class="ui container fluid">
<h4 class="ui top attached header center">
{{if .LinkAccountMode}}
{{ctx.Locale.Tr "auth.oauth_signup_title"}}
{{else}}
{{ctx.Locale.Tr "sign_up"}}
{{end}}
{{ctx.Locale.Tr "gpg.signup.title"}}
</h4>
<div class="ui attached segment">
{{if .IsFirstTimeRegistration}}
<p>{{ctx.Locale.Tr "auth.sign_up_tip"}}</p>
{{end}}
<form class="ui form" action="{{.SignUpLink}}" method="post">
{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}}
{{template "base/alert" .}}
{{end}}
{{if .DisableRegistration}}
<p>{{ctx.Locale.Tr "auth.disable_register_prompt"}}</p>
{{else}}
<div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
<label for="user_name">{{ctx.Locale.Tr "username"}}</label>
<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
<form class="ui form" action="{{.SignUpLink}}" method="post" id="signup-form">
{{.CsrfTokenHtml}}
<input type="hidden" id="hidden-gpg-key" name="gpg_key">
<input type="hidden" id="hidden-nonce" name="nonce">
<div id="step-key" {{if .Err_GPGSign}}style="display: none;"{{end}}>
{{template "base/alert" .}}
<div class="required field {{if .Err_GPGKey}}error{{end}}">
<label>{{ctx.Locale.Tr "gpg.signup.paste_key"}}</label>
<textarea id="gpg_key" rows="7" placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----" style="font-family: monospace;" required>{{.gpg_key}}</textarea>
</div>
<div class="required field {{if .Err_Email}}error{{end}}">
<label for="email">{{ctx.Locale.Tr "email"}}</label>
<input id="email" name="email" type="email" value="{{.email}}" required>
</div>
{{if not .DisablePassword}}
<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
<label for="password">{{ctx.Locale.Tr "password"}}</label>
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="new-password" required>
</div>
<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
<label for="retype">{{ctx.Locale.Tr "re_type"}}</label>
<input id="retype" name="retype" type="password" value="{{.retype}}" autocomplete="new-password" required>
</div>
{{end}}
{{template "user/auth/captcha" .}}
<div class="inline field">
<button class="ui primary button tw-w-full">
{{if .LinkAccountMode}}
{{ctx.Locale.Tr "auth.oauth_signup_submit"}}
{{else}}
{{ctx.Locale.Tr "auth.create_new_account"}}
{{end}}
</button>
<button class="ui primary button tw-w-full" type="button" id="btn-proceed">{{ctx.Locale.Tr "gpg.signup.proceed"}}</button>
</div>
{{end}}
{{$showExternalAuthMethods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
{{if $showExternalAuthMethods}}
<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
{{template "user/auth/external_auth_methods" .}}
{{end}}
</div>
<div id="step-sign" {{if not .Err_GPGSign}}style="display: none;"{{end}}>
{{template "base/alert" .}}
<div class="field">
<label>{{ctx.Locale.Tr "gpg.signin.nonce_label"}}</label>
<input id="token-field" type="text" readonly style="font-family: monospace;">
</div>
<div class="ui info message">
<p>{{ctx.Locale.Tr "gpg.signin.command_hint"}}</p>
<div style="display: flex; gap: 6px;">
<input id="sign-command" type="text" readonly style="font-family: monospace; min-width: 0; flex: 1;">
<button class="ui button" type="button" id="btn-copy-cmd">{{ctx.Locale.Tr "gpg.signin.copy"}}</button>
</div>
</div>
<div class="required field {{if .Err_GPGSign}}error{{end}}">
<label for="gpg_signature">{{ctx.Locale.Tr "gpg.signin.signed_output"}}</label>
<textarea id="gpg_signature" name="gpg_signature" rows="7" placeholder="-----BEGIN PGP SIGNED MESSAGE-----" style="font-family: monospace;" required>{{.gpg_signature}}</textarea>
</div>
<div class="inline field">
<button class="ui primary button tw-w-full" type="submit">{{ctx.Locale.Tr "gpg.signup.submit"}}</button>
</div>
</div>
</form>
</div>
</div>
<div class="ui container fluid">
{{if not .LinkAccountMode}}
<div class="ui attached segment header top tw-flex tw-flex-col tw-items-center">
<div class="field">
<span>{{ctx.Locale.Tr "auth.already_have_account"}}</span>
<a href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "auth.sign_in_now"}}</a>
<span>{{ctx.Locale.Tr "gpg.signup.already_have_account"}}</span>
<a href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "gpg.signin.title"}}</a>
</div>
</div>
{{end}}
</div>
</div>
+5 -5
View File
@@ -56,9 +56,9 @@
</div>
<div class="flex-text-block">
{{if not .IsPrimary}}
<button class="ui red tiny button delete-button" data-modal-id="delete-email" data-url="{{AppSubUrl}}/user/settings/account/email/delete" data-id="{{.ID}}">
<!-- <button class="ui red tiny button delete-button" data-modal-id="delete-email" data-url="{{AppSubUrl}}/user/settings/account/email/delete" data-id="{{.ID}}">
{{ctx.Locale.Tr "settings.delete_email"}}
</button>
</button> m8sh automatically removes emails when related keys are removed -->
{{if .CanBePrimary}}
<form action="{{AppSubUrl}}/user/settings/account/email" method="post">
<input name="_method" type="hidden" value="PRIMARY">
@@ -74,7 +74,7 @@
{{if $.ActivationsPending}}
<button disabled class="ui primary tiny button">{{ctx.Locale.Tr "settings.activations_pending"}}</button>
{{else}}
<button class="ui primary tiny button">{{ctx.Locale.Tr "settings.activate_email"}}</button>
<!-- <button class="ui primary tiny button">{{ctx.Locale.Tr "settings.activate_email"}}</button> -->
{{end}}
</form>
{{end}}
@@ -86,7 +86,7 @@
</div>
{{end}}
{{if not ($.UserDisabledFeatures.Contains "manage_credentials")}}
<!-- {{if not ($.UserDisabledFeatures.Contains "manage_credentials")}}
<div class="ui bottom attached segment">
<form class="ui form" action="{{AppSubUrl}}/user/settings/account/email" method="post">
<div class="required field {{if .Err_Email}}error{{end}}">
@@ -102,7 +102,7 @@
<div class="ui warning message">{{ctx.Locale.Tr "settings.can_not_add_email_activations_pending"}}</div>
{{end}}
</div>
{{end}}
{{end}} in m8sh emails added via GPG keys, containing them -->
{{if not ($.UserDisabledFeatures.Contains "deletion")}}
<h4 class="ui top attached error header">
+2 -2
View File
@@ -86,10 +86,10 @@
</details>
</div>
{{if .EnableOAuth2}}
<!-- {{if .EnableOAuth2}}
{{template "user/settings/grants_oauth2" .}}
{{template "user/settings/applications_oauth2" .}}
{{end}}
{{end}} OAuth disabled in m8sh... -->
</div>
<div class="ui g-modal-confirm delete modal" id="delete-token">
+32 -19
View File
@@ -6,8 +6,9 @@
</h4>
<div class="ui attached segment">
<div class="{{if not .HasGPGError}}tw-hidden{{end}} tw-mb-4" id="add-gpg-key-panel">
<form class="ui form{{if .HasGPGError}} error{{end}}" action="{{.Link}}" method="post">
<form class="ui form{{if .HasGPGError}} error{{end}}" id="add-key-form" action="{{.Link}}" method="post">
<input type="hidden" name="title" value="none">
<input type="hidden" id="add-key-hidden-nonce" name="nonce">
<div class="field {{if .Err_Content}}error{{end}}">
<label for="gpg-key-content">{{ctx.Locale.Tr "settings.key_content"}}</label>
<textarea id="gpg-key-content" name="content" placeholder="{{ctx.Locale.Tr "settings.key_content_gpg_placeholder"}}" required>{{.content}}</textarea>
@@ -17,16 +18,19 @@
<p>{{ctx.Locale.Tr "settings.gpg_token_required"}}</p>
</div>
<div class="field">
<label for="token">{{ctx.Locale.Tr "settings.gpg_token"}}</label>
<input readonly="" value="{{.TokenToSign}}">
<div class="help">
{{ctx.Locale.Tr "settings.gpg_token_help"}}
<pre class="command-block">{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` .TokenToSign .PaddedKeyID}}</pre>
<label>{{ctx.Locale.Tr "settings.gpg_token"}}</label>
<input id="add-key-token-field" type="text" readonly style="font-family: monospace;">
</div>
<div class="ui info message">
<p>{{ctx.Locale.Tr "settings.gpg_token_help"}}</p>
<div style="display: flex; gap: 6px;">
<input id="add-key-sign-command" type="text" readonly style="font-family: monospace; min-width: 0; flex: 1;">
<button class="ui button" type="button" id="add-key-btn-copy-cmd">{{ctx.Locale.Tr "settings.gpg_sign_copy"}}</button>
</div>
</div>
<div class="field">
<label for="gpg-key-signature">{{ctx.Locale.Tr "settings.gpg_token_signature"}}</label>
<textarea id="gpg-key-signature" name="signature" placeholder="{{ctx.Locale.Tr "settings.key_signature_gpg_placeholder"}}" required>{{.signature}}</textarea>
<textarea id="gpg-key-signature" name="signature" placeholder="-----BEGIN PGP SIGNATURE-----" style="font-family: monospace;" required>{{.signature}}</textarea>
</div>
{{end}}
<input name="type" type="hidden" value="gpg">
@@ -68,9 +72,15 @@
</div>
</div>
<div class="item-trailing">
<button class="ui red tiny button delete-button" data-modal-id="delete-gpg" data-url="{{$.Link}}/delete?type=gpg" data-id="{{.ID}}">
{{ctx.Locale.Tr "settings.delete_key"}}
</button>
{{if gt (len $.GPGKeys) 1}}
<button class="ui red tiny button delete-button" data-modal-id="delete-gpg" data-url="{{$.Link}}/delete?type=gpg" data-id="{{.ID}}">
{{ctx.Locale.Tr "settings.delete_key"}}
</button>
{{else}}
<button class="ui red tiny button disabled" disabled title="{{ctx.Locale.Tr "settings.gpg_last_key_delete"}}">
{{ctx.Locale.Tr "settings.delete_key"}}
</button>
{{end}}
{{if and (not .Verified) (ne $.VerifyingID .KeyID)}}
<a class="ui primary tiny button" href="?verify_gpg={{.KeyID}}">{{ctx.Locale.Tr "settings.gpg_key_verify"}}</a>
{{end}}
@@ -79,22 +89,25 @@
{{if and (not .Verified) (eq $.VerifyingID .KeyID)}}
<div class="ui segment">
<h4>{{ctx.Locale.Tr "settings.gpg_token_required"}}</h4>
<form class="ui form{{if $.HasGPGVerifyError}} error{{end}}" action="{{$.Link}}" method="post">
<form class="ui form{{if $.HasGPGVerifyError}} error{{end}}" id="verify-key-form" data-key-id="{{.PaddedKeyID}}" action="{{$.Link}}" method="post">
<input type="hidden" name="title" value="none">
<input type="hidden" name="content" value="{{.KeyID}}">
<input type="hidden" name="key_id" value="{{.KeyID}}">
<input type="hidden" id="verify-key-hidden-nonce" name="nonce">
<div class="field">
<label for="token">{{ctx.Locale.Tr "settings.gpg_token"}}</label>
<input readonly="" value="{{$.TokenToSign}}">
<div class="help">
{{ctx.Locale.Tr "settings.gpg_token_help"}}
<pre class="command-block">{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` $.TokenToSign .PaddedKeyID}}</pre>
<label>{{ctx.Locale.Tr "settings.gpg_token"}}</label>
<input id="verify-key-token-field" type="text" readonly style="font-family: monospace;">
</div>
<div class="ui info message">
<p>{{ctx.Locale.Tr "settings.gpg_token_help"}}</p>
<div style="display: flex; gap: 6px;">
<input id="verify-key-sign-command" type="text" readonly style="font-family: monospace; min-width: 0; flex: 1;">
<button class="ui button" type="button" id="verify-key-btn-copy-cmd">{{ctx.Locale.Tr "settings.gpg_sign_copy"}}</button>
</div>
<br>
</div>
<div class="field">
<label for="signature">{{ctx.Locale.Tr "settings.gpg_token_signature"}}</label>
<textarea id="gpg-key-signature" name="signature" placeholder="{{ctx.Locale.Tr "settings.key_signature_gpg_placeholder"}}" required>{{$.signature}}</textarea>
<label for="gpg-key-signature">{{ctx.Locale.Tr "settings.gpg_token_signature"}}</label>
<textarea id="gpg-key-signature" name="signature" placeholder="-----BEGIN PGP SIGNATURE-----" style="font-family: monospace;" required>{{$.signature}}</textarea>
</div>
<input name="type" type="hidden" value="verify_gpg">
<button class="ui primary button">
+2 -2
View File
@@ -17,11 +17,11 @@
<a class="{{if .PageIsSettingsAppearance}}active {{end}}item" href="{{AppSubUrl}}/user/settings/appearance">
{{ctx.Locale.Tr "settings.appearance"}}
</a>
{{if not ($.UserDisabledFeatures.Contains "manage_mfa" "manage_credentials")}}
<!-- {{if not ($.UserDisabledFeatures.Contains "manage_mfa" "manage_credentials")}}
<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security">
{{ctx.Locale.Tr "settings.security"}}
</a>
{{end}}
{{end}} m8sh has GPG security by default -->
<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
{{ctx.Locale.Tr "user.block.list"}}
</a>
+58
View File
@@ -0,0 +1,58 @@
export function generateNonce(): string {
const existing = sessionStorage.getItem('gpg_signup_nonce');
if (existing) return existing;
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;
}
export function buildSignCommand(nonce: string, keyID?: string): string {
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;
}
+3 -25
View File
@@ -27,14 +27,12 @@ function initPreInstall() {
const dbUser = document.querySelector<HTMLInputElement>('#db_user')!;
const dbName = document.querySelector<HTMLInputElement>('#db_name')!;
// Database type change detection.
document.querySelector<HTMLInputElement>('#db_type')!.addEventListener('change', function () {
const dbType = this.value;
hideElem('div[data-db-setting-for]');
showElem(`div[data-db-setting-for=${dbType}]`);
if (dbType !== 'sqlite3') {
// for most remote database servers
showElem('div[data-db-setting-for=common-host]');
const lastDbHost = dbHost.value;
const isDbHostDefault = !lastDbHost || Object.values(defaultDbHosts).includes(lastDbHost);
@@ -45,7 +43,7 @@ function initPreInstall() {
dbUser.value = defaultDbUser;
dbName.value = defaultDbName;
}
} // else: for SQLite3, the default path is always prepared by backend code (setting)
}
});
document.querySelector('#db_type')!.dispatchEvent(new Event('change'));
@@ -59,28 +57,8 @@ function initPreInstall() {
domain.value = window.location.hostname;
}
// TODO: better handling of exclusive relations.
document.querySelector<HTMLInputElement>('#enable-openid-signin input')!.addEventListener('change', function () {
if (this.checked) {
if (!document.querySelector<HTMLInputElement>('#disable-registration input')!.checked) {
document.querySelector<HTMLInputElement>('#enable-openid-signup input')!.checked = true;
}
} else {
document.querySelector<HTMLInputElement>('#enable-openid-signup input')!.checked = false;
}
});
document.querySelector<HTMLInputElement>('#disable-registration input')!.addEventListener('change', function () {
if (this.checked) {
document.querySelector<HTMLInputElement>('#enable-captcha input')!.checked = false;
document.querySelector<HTMLInputElement>('#enable-openid-signup input')!.checked = false;
} else {
document.querySelector<HTMLInputElement>('#enable-openid-signup input')!.checked = true;
}
});
document.querySelector<HTMLInputElement>('#enable-captcha input')!.addEventListener('change', function () {
if (this.checked) {
document.querySelector<HTMLInputElement>('#disable-registration input')!.checked = false;
}
document.querySelector<HTMLInputElement>('#enable-captcha input')?.addEventListener('change', () => {
// captcha toggle logic if needed
});
}
@@ -0,0 +1,22 @@
import {initGpgNonceWidget, validateSignature} from './gpg-nonce.ts';
export function initGpgSignin() {
initGpgNonceWidget({
tokenFieldId: 'token-field',
nonceInputId: 'hidden-nonce',
signCommandId: 'sign-command',
copyBtnId: 'btn-copy-cmd',
signatureId: 'gpg_signature',
formId: 'signin-form',
});
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');
});
}
@@ -0,0 +1,47 @@
import { generateNonce, buildSignCommand, validateSignature } from './gpg-nonce.ts';
export function initGpgSignup() {
const btnProceed = document.querySelector('#btn-proceed');
if (!btnProceed) return;
const nonce = generateNonce();
btnProceed.addEventListener('click', () => {
const key = (document.querySelector('#gpg_key') as HTMLTextAreaElement).value.trim();
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('#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', () => {
navigator.clipboard.writeText(
(document.querySelector('#sign-command') as HTMLInputElement).value,
);
});
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');
});
}
@@ -0,0 +1,27 @@
import { initGpgNonceWidget } 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',
});
// 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,
});
}
+6
View File
@@ -66,6 +66,9 @@ import {initActionsPermissionsForm} from './features/common-actions-permissions.
import {initRefIssueContextPopup} from './features/ref-issue.ts';
import {initGlobalShortcut} from './modules/shortcut.ts';
import {initDevtest} from './modules/devtest.ts';
import {initGpgSignup} from './features/user-auth-gpg-signup.ts';
import {initGpgSignin} from './features/user-auth-gpg-signin.ts';
import {initGpgKeySettings} from './features/user-settings-gpg-key.ts';
const initStartTime = performance.now();
const initPerformanceTracer = callInitFunctions([
@@ -84,6 +87,9 @@ const initPerformanceTracer = callInitFunctions([
initGlobalDeleteButton,
initGlobalInput,
initGlobalShortcut,
initGpgSignup,
initGpgSignin,
initGpgKeySettings,
initCommonOrganization,
initCommonIssueListQuickGoto,