Federated GPG-based auth #3
@@ -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
|
||||
-->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "Έχετε ήδη λογαριασμό;"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "قبلاً حساب دارید؟"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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 qu’il 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"
|
||||
}
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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": "対象リポジトリの追加"
|
||||
}
|
||||
}
|
||||
@@ -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": "대상 리포지토리 추가"
|
||||
}
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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": "Подмодуль"
|
||||
}
|
||||
}
|
||||
@@ -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": "දැනටමත් ගිණුමක් තිබේද?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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ı?"
|
||||
}
|
||||
@@ -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": "Вже маєте акаунт?"
|
||||
}
|
||||
@@ -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": "添加目标仓库"
|
||||
}
|
||||
}
|
||||
@@ -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": "已有帳戶?"
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 *****
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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}}
|
||||
|
||||
@@ -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}}
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user