Rework SSH key management UI to add GPG (#1293)
* Rework SSH key management UI to add GPG * Add more detail to gpg key display * Update CHANGELOG.md * Implement deletion UI * Implement adding gpg UI * Various fixes - Fix duplicate entry in locale - Re-generate hash before verification since they are consumed * Add missing translation * Split template * Catch not found/verified email errorrelease/v1.15
parent
b7da5a6cb7
commit
8371f94d06
|
@ -4,6 +4,8 @@
|
|||
|
||||
* BREAKING
|
||||
* Password reset URL changed from `/user/forget_password` to `/user/forgot_password`
|
||||
* SSH keys management URL changed from `/user/settings/ssh` to `/user/settings/keys`
|
||||
|
||||
|
||||
## [1.1.0](https://github.com/go-gitea/gitea/releases/tag/v1.1.0) - 2017-03-09
|
||||
|
||||
|
|
|
@ -260,6 +260,36 @@ func (err ErrKeyNameAlreadyUsed) Error() string {
|
|||
return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
|
||||
}
|
||||
|
||||
// ErrGPGEmailNotFound represents a "ErrGPGEmailNotFound" kind of error.
|
||||
type ErrGPGEmailNotFound struct {
|
||||
Email string
|
||||
}
|
||||
|
||||
// IsErrGPGEmailNotFound checks if an error is a ErrGPGEmailNotFound.
|
||||
func IsErrGPGEmailNotFound(err error) bool {
|
||||
_, ok := err.(ErrGPGEmailNotFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrGPGEmailNotFound) Error() string {
|
||||
return fmt.Sprintf("failed to found email or is not confirmed : %s", err.Email)
|
||||
}
|
||||
|
||||
// ErrGPGKeyParsing represents a "ErrGPGKeyParsing" kind of error.
|
||||
type ErrGPGKeyParsing struct {
|
||||
ParseError error
|
||||
}
|
||||
|
||||
// IsErrGPGKeyParsing checks if an error is a ErrGPGKeyParsing.
|
||||
func IsErrGPGKeyParsing(err error) bool {
|
||||
_, ok := err.(ErrGPGKeyParsing)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrGPGKeyParsing) Error() string {
|
||||
return fmt.Sprintf("failed to parse gpg key %s", err.ParseError.Error())
|
||||
}
|
||||
|
||||
// ErrGPGKeyNotExist represents a "GPGKeyNotExist" kind of error.
|
||||
type ErrGPGKeyNotExist struct {
|
||||
ID int64
|
||||
|
|
|
@ -89,7 +89,7 @@ func GetGPGKeyByID(keyID int64) (*GPGKey, error) {
|
|||
func checkArmoredGPGKeyString(content string) (*openpgp.Entity, error) {
|
||||
list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, ErrGPGKeyParsing{err}
|
||||
}
|
||||
return list[0], nil
|
||||
}
|
||||
|
@ -219,7 +219,7 @@ func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) {
|
|||
}
|
||||
}
|
||||
if emails[n] == nil {
|
||||
return nil, fmt.Errorf("Failed to found email or is not confirmed : %s", ident.UserId.Email)
|
||||
return nil, ErrGPGEmailNotFound{ident.UserId.Email}
|
||||
}
|
||||
n++
|
||||
}
|
||||
|
@ -400,17 +400,16 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
|
|||
}
|
||||
}
|
||||
|
||||
//Generating hash of commit
|
||||
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
|
||||
if err != nil { //Skipping ailed to generate hash
|
||||
log.Error(3, "PopulateHash: %v", err)
|
||||
return &CommitVerification{
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
//Generating hash of commit
|
||||
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
|
||||
if err != nil { //Skipping ailed to generate hash
|
||||
log.Error(3, "PopulateHash: %v", err)
|
||||
return &CommitVerification{
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
//We get PK
|
||||
if err := verifySign(sig, hash, k); err == nil {
|
||||
return &CommitVerification{ //Everything is ok
|
||||
|
@ -422,6 +421,16 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
|
|||
}
|
||||
//And test also SubsKey
|
||||
for _, sk := range k.SubsKey {
|
||||
|
||||
//Generating hash of commit
|
||||
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
|
||||
if err != nil { //Skipping ailed to generate hash
|
||||
log.Error(3, "PopulateHash: %v", err)
|
||||
return &CommitVerification{
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
if err := verifySign(sig, hash, sk); err == nil {
|
||||
return &CommitVerification{ //Everything is ok
|
||||
Verified: true,
|
||||
|
|
|
@ -163,14 +163,15 @@ func (f *AddOpenIDForm) Validate(ctx *macaron.Context, errs binding.Errors) bind
|
|||
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// AddSSHKeyForm form for adding SSH key
|
||||
type AddSSHKeyForm struct {
|
||||
// AddKeyForm form for adding SSH/GPG key
|
||||
type AddKeyForm struct {
|
||||
Type string `binding:"OmitEmpty"`
|
||||
Title string `binding:"Required;MaxSize(50)"`
|
||||
Content string `binding:"Required"`
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *AddSSHKeyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||
func (f *AddKeyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
|
|
|
@ -258,6 +258,7 @@ user_not_exist = The given user does not exist.
|
|||
last_org_owner = Removing the last user from a owner team isn't allowed because there must always be at least one owner in any given organization.
|
||||
|
||||
invalid_ssh_key = Sorry, we're not able to verify your SSH key: %s
|
||||
invalid_gpg_key = Sorry, we're not able to verify your GPG key: %s
|
||||
unable_verify_ssh_key = Gitea cannot verify your SSH key, but we are assuming that it is valid, please double-check it.
|
||||
auth_failed = Authentication failed: %v
|
||||
|
||||
|
@ -285,7 +286,7 @@ form.name_pattern_not_allowed = Username pattern '%s' is not allowed.
|
|||
profile = Profile
|
||||
password = Password
|
||||
avatar = Avatar
|
||||
ssh_keys = SSH Keys
|
||||
ssh_gpg_keys = SSH / GPG Keys
|
||||
social = Social Accounts
|
||||
applications = Applications
|
||||
orgs = Organizations
|
||||
|
@ -349,20 +350,33 @@ keep_email_private_popup = Your email address will be hidden from other users if
|
|||
openid_desc = Your OpenID addresses will let you delegate authentication to your provider of choice
|
||||
|
||||
manage_ssh_keys = Manage SSH Keys
|
||||
manage_gpg_keys = Manage GPG Keys
|
||||
add_key = Add Key
|
||||
ssh_desc = This is a list of SSH keys associated with your account. Because these keys allow anyone using them to gain access to your repositories, it is highly important that you make sure you recognize them.
|
||||
gpg_desc = This is a list of GPG keys associated with your account. Because these keys allow verification of commit, it is highly important that you keep safe the corresponding private key.
|
||||
ssh_helper = <strong>Don't know how?</strong> Check out GitHub's guide to <a href="%s">create your own SSH keys</a> or solve <a href="%s">common problems</a> you might encounter using SSH.
|
||||
gpg_helper = <strong>Don't know how?</strong> Check out GitHub's guide <a href="%s">about GPG</a>.
|
||||
add_new_key = Add SSH Key
|
||||
add_new_gpg_key = Add GPG Key
|
||||
ssh_key_been_used = Public key content has already been used.
|
||||
ssh_key_name_used = Public key with same name already exists.
|
||||
gpg_key_id_used = Public GPG key with same id already exists.
|
||||
gpg_key_email_not_found = The email attached to the GPG key couldn't be found or is not yet confirmed: %s
|
||||
subkeys = Subkeys
|
||||
key_id = Key ID
|
||||
key_name = Key Name
|
||||
key_content = Content
|
||||
add_key_success = Your new SSH key '%s' has been added successfully!
|
||||
add_gpg_key_success = Your new GPG key '%s' has been added successfully!
|
||||
delete_key = Delete
|
||||
ssh_key_deletion = SSH Key Deletion
|
||||
gpg_key_deletion = GPG Key Deletion
|
||||
ssh_key_deletion_desc = Delete this SSH key will disable all access using this SSH key for your account. Do you want to continue?
|
||||
gpg_key_deletion_desc = Delete this GPG key will disable all commit verification sign with this GPG key. Do you want to continue?
|
||||
ssh_key_deletion_success = SSH key has been deleted successfully!
|
||||
gpg_key_deletion_success = GPG key has been deleted successfully!
|
||||
add_on = Added on
|
||||
valid_until = Valid until
|
||||
last_used = Last used on
|
||||
no_activity = No recent activity
|
||||
key_state_desc = This key is used in last 7 days
|
||||
|
@ -1364,7 +1378,6 @@ mark_as_unread = Mark as unread
|
|||
error.extract_sign = Failed to extract signature
|
||||
error.generate_hash = Failed to generate hash of commit
|
||||
error.no_committer_account = No account linked to committer email
|
||||
error.no_gpg_keys_found = "Failed to retrieve publics keys of committer"
|
||||
error.no_gpg_keys_found = "No known key found for this signature in database"
|
||||
error.not_signed_commit = "Not a signed commit"
|
||||
error.failed_retrieval_gpg_keys = "Failed to retrieve any key attached to the commiter account"
|
||||
|
|
|
@ -1408,7 +1408,11 @@ $(document).ready(function () {
|
|||
// Helpers.
|
||||
$('.delete-button').click(function () {
|
||||
var $this = $(this);
|
||||
$('.delete.modal').modal({
|
||||
var filter = "";
|
||||
if ($this.attr("id")) {
|
||||
filter += "#"+$this.attr("id")
|
||||
}
|
||||
$('.delete.modal'+filter).modal({
|
||||
closable: false,
|
||||
onApprove: function () {
|
||||
if ($this.data('type') == "form") {
|
||||
|
|
|
@ -664,7 +664,7 @@ func DeployKeys(ctx *context.Context) {
|
|||
}
|
||||
|
||||
// DeployKeysPost response for adding a deploy key of a repository
|
||||
func DeployKeysPost(ctx *context.Context, form auth.AddSSHKeyForm) {
|
||||
func DeployKeysPost(ctx *context.Context, form auth.AddKeyForm) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
|
||||
ctx.Data["PageIsSettingsKeys"] = true
|
||||
|
||||
|
|
|
@ -214,9 +214,9 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
})
|
||||
}
|
||||
|
||||
m.Combo("/ssh").Get(user.SettingsSSHKeys).
|
||||
Post(bindIgnErr(auth.AddSSHKeyForm{}), user.SettingsSSHKeysPost)
|
||||
m.Post("/ssh/delete", user.DeleteSSHKey)
|
||||
m.Combo("/keys").Get(user.SettingsKeys).
|
||||
Post(bindIgnErr(auth.AddKeyForm{}), user.SettingsKeysPost)
|
||||
m.Post("/keys/delete", user.DeleteKey)
|
||||
m.Combo("/applications").Get(user.SettingsApplications).
|
||||
Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
|
||||
m.Post("/applications/delete", user.SettingsDeleteApplication)
|
||||
|
@ -438,7 +438,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
|
||||
m.Group("/keys", func() {
|
||||
m.Combo("").Get(repo.DeployKeys).
|
||||
Post(bindIgnErr(auth.AddSSHKeyForm{}), repo.DeployKeysPost)
|
||||
Post(bindIgnErr(auth.AddKeyForm{}), repo.DeployKeysPost)
|
||||
m.Post("/delete", repo.DeleteDeployKey)
|
||||
})
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ const (
|
|||
tplSettingsAvatar base.TplName = "user/settings/avatar"
|
||||
tplSettingsPassword base.TplName = "user/settings/password"
|
||||
tplSettingsEmails base.TplName = "user/settings/email"
|
||||
tplSettingsSSHKeys base.TplName = "user/settings/sshkeys"
|
||||
tplSettingsKeys base.TplName = "user/settings/keys"
|
||||
tplSettingsSocial base.TplName = "user/settings/social"
|
||||
tplSettingsApplications base.TplName = "user/settings/applications"
|
||||
tplSettingsTwofa base.TplName = "user/settings/twofa"
|
||||
|
@ -320,10 +320,10 @@ func DeleteEmail(ctx *context.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
// SettingsSSHKeys render user's SSH public keys page
|
||||
func SettingsSSHKeys(ctx *context.Context) {
|
||||
// SettingsKeys render user's SSH/GPG public keys page
|
||||
func SettingsKeys(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["PageIsSettingsSSHKeys"] = true
|
||||
ctx.Data["PageIsSettingsKeys"] = true
|
||||
|
||||
keys, err := models.ListPublicKeys(ctx.User.ID)
|
||||
if err != nil {
|
||||
|
@ -332,13 +332,20 @@ func SettingsSSHKeys(ctx *context.Context) {
|
|||
}
|
||||
ctx.Data["Keys"] = keys
|
||||
|
||||
ctx.HTML(200, tplSettingsSSHKeys)
|
||||
gpgkeys, err := models.ListGPGKeys(ctx.User.ID)
|
||||
if err != nil {
|
||||
ctx.Handle(500, "ListGPGKeys", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["GPGKeys"] = gpgkeys
|
||||
|
||||
ctx.HTML(200, tplSettingsKeys)
|
||||
}
|
||||
|
||||
// SettingsSSHKeysPost response for change user's SSH keys
|
||||
func SettingsSSHKeysPost(ctx *context.Context, form auth.AddSSHKeyForm) {
|
||||
// SettingsKeysPost response for change user's SSH/GPG keys
|
||||
func SettingsKeysPost(ctx *context.Context, form auth.AddKeyForm) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["PageIsSettingsSSHKeys"] = true
|
||||
ctx.Data["PageIsSettingsKeys"] = true
|
||||
|
||||
keys, err := models.ListPublicKeys(ctx.User.ID)
|
||||
if err != nil {
|
||||
|
@ -347,51 +354,97 @@ func SettingsSSHKeysPost(ctx *context.Context, form auth.AddSSHKeyForm) {
|
|||
}
|
||||
ctx.Data["Keys"] = keys
|
||||
|
||||
gpgkeys, err := models.ListGPGKeys(ctx.User.ID)
|
||||
if err != nil {
|
||||
ctx.Handle(500, "ListGPGKeys", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["GPGKeys"] = gpgkeys
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(200, tplSettingsSSHKeys)
|
||||
ctx.HTML(200, tplSettingsKeys)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := models.CheckPublicKeyString(form.Content)
|
||||
if err != nil {
|
||||
if models.IsErrKeyUnableVerify(err) {
|
||||
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
|
||||
} else {
|
||||
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/ssh")
|
||||
switch form.Type {
|
||||
case "gpg":
|
||||
key, err := models.AddGPGKey(ctx.User.ID, form.Content)
|
||||
if err != nil {
|
||||
ctx.Data["HasGPGError"] = true
|
||||
switch {
|
||||
case models.IsErrGPGKeyParsing(err):
|
||||
ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error()))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||
case models.IsErrGPGKeyIDAlreadyUsed(err):
|
||||
ctx.Data["Err_Content"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
|
||||
case models.IsErrGPGEmailNotFound(err):
|
||||
ctx.Data["Err_Content"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("settings.gpg_key_email_not_found", err.(models.ErrGPGEmailNotFound).Email), tplSettingsKeys, &form)
|
||||
default:
|
||||
ctx.Handle(500, "AddPublicKey", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil {
|
||||
ctx.Data["HasError"] = true
|
||||
switch {
|
||||
case models.IsErrKeyAlreadyExist(err):
|
||||
ctx.Data["Err_Content"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsSSHKeys, &form)
|
||||
case models.IsErrKeyNameAlreadyUsed(err):
|
||||
ctx.Data["Err_Title"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsSSHKeys, &form)
|
||||
default:
|
||||
ctx.Handle(500, "AddPublicKey", err)
|
||||
ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", key.KeyID))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||
case "ssh":
|
||||
content, err := models.CheckPublicKeyString(form.Content)
|
||||
if err != nil {
|
||||
if models.IsErrKeyUnableVerify(err) {
|
||||
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
|
||||
} else {
|
||||
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil {
|
||||
ctx.Data["HasSSHError"] = true
|
||||
switch {
|
||||
case models.IsErrKeyAlreadyExist(err):
|
||||
ctx.Data["Err_Content"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
|
||||
case models.IsErrKeyNameAlreadyUsed(err):
|
||||
ctx.Data["Err_Title"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
|
||||
default:
|
||||
ctx.Handle(500, "AddPublicKey", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||
|
||||
default:
|
||||
ctx.Flash.Warning("Function not implemented")
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/ssh")
|
||||
}
|
||||
|
||||
// DeleteSSHKey response for delete user's SSH key
|
||||
func DeleteSSHKey(ctx *context.Context) {
|
||||
if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil {
|
||||
ctx.Flash.Error("DeletePublicKey: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
|
||||
}
|
||||
// DeleteKey response for delete user's SSH/GPG key
|
||||
func DeleteKey(ctx *context.Context) {
|
||||
|
||||
switch ctx.Query("type") {
|
||||
case "gpg":
|
||||
if err := models.DeleteGPGKey(ctx.User, ctx.QueryInt64("id")); err != nil {
|
||||
ctx.Flash.Error("DeleteGPGKey: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
|
||||
}
|
||||
case "ssh":
|
||||
if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil {
|
||||
ctx.Flash.Error("DeletePublicKey: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
|
||||
}
|
||||
default:
|
||||
ctx.Flash.Warning("Function not implemented")
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"redirect": setting.AppSubURL + "/user/settings/ssh",
|
||||
"redirect": setting.AppSubURL + "/user/settings/keys",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{{template "base/head" .}}
|
||||
<div class="user settings sshkeys">
|
||||
{{template "user/settings/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
{{template "user/settings/keys_ssh" .}}
|
||||
<br>
|
||||
{{template "user/settings/keys_gpg" .}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "base/footer" .}}
|
|
@ -0,0 +1,67 @@
|
|||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "settings.manage_gpg_keys"}}
|
||||
<div class="ui right">
|
||||
<div class="ui blue tiny show-panel button" data-panel="#add-gpg-key-panel">{{.i18n.Tr "settings.add_key"}}</div>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<div class="ui key list">
|
||||
<div class="item">
|
||||
{{.i18n.Tr "settings.gpg_desc"}}
|
||||
</div>
|
||||
{{range .GPGKeys}}
|
||||
<div class="item">
|
||||
<div class="right floated content">
|
||||
<button class="ui red tiny button delete-button" id="delete-gpg" data-url="{{$.Link}}/delete?type=gpg" data-id="{{.ID}}">
|
||||
{{$.i18n.Tr "settings.delete_key"}}
|
||||
</button>
|
||||
</div>
|
||||
<i class="mega-octicon octicon-key {{if .Expired.After $.PageStartTime}}green{{end}}"></i>
|
||||
<div class="content">
|
||||
{{range .Emails}}<strong>{{.Email}} </strong>{{end}}
|
||||
<div class="print meta">
|
||||
<b>{{$.i18n.Tr "settings.key_id"}}:</b> {{.KeyID}}
|
||||
<b>{{$.i18n.Tr "settings.subkeys"}}:</b> {{range .SubsKey}} {{.KeyID}} {{end}}
|
||||
</div>
|
||||
<div class="activity meta">
|
||||
<i>{{$.i18n.Tr "settings.add_on"}} <span>{{DateFmtShort .Added}}</span></i>
|
||||
-
|
||||
<i>{{$.i18n.Tr "settings.valid_until"}} <span>{{DateFmtShort .Expired}}</span></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<p>{{.i18n.Tr "settings.gpg_helper" "https://help.github.com/articles/about-gpg/" | Str2html}}</p>
|
||||
<div {{if not .HasGPGError}}class="hide"{{end}} id="add-gpg-key-panel">
|
||||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "settings.add_new_gpg_key"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="title" value="none">
|
||||
<div class="field {{if .Err_Content}}error{{end}}">
|
||||
<label for="content">{{.i18n.Tr "settings.key_content"}}</label>
|
||||
<textarea id="gpg-key-content" name="content" required>{{.content}}</textarea>
|
||||
</div>
|
||||
<input name="type" type="hidden" value="gpg">
|
||||
<button class="ui green button">
|
||||
{{.i18n.Tr "settings.add_key"}}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui small basic delete modal" id="delete-gpg">
|
||||
<div class="ui icon header">
|
||||
<i class="trash icon"></i>
|
||||
{{.i18n.Tr "settings.gpg_key_deletion"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{.i18n.Tr "settings.gpg_key_deletion_desc"}}</p>
|
||||
</div>
|
||||
{{template "base/delete_modal_actions" .}}
|
||||
</div>
|
|
@ -0,0 +1,67 @@
|
|||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "settings.manage_ssh_keys"}}
|
||||
<div class="ui right">
|
||||
<div class="ui blue tiny show-panel button" data-panel="#add-ssh-key-panel">{{.i18n.Tr "settings.add_key"}}</div>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<div class="ui key list">
|
||||
<div class="item">
|
||||
{{.i18n.Tr "settings.ssh_desc"}}
|
||||
</div>
|
||||
{{range .Keys}}
|
||||
<div class="item">
|
||||
<div class="right floated content">
|
||||
<button class="ui red tiny button delete-button" id="delete-ssh" data-url="{{$.Link}}/delete?type=ssh" data-id="{{.ID}}">
|
||||
{{$.i18n.Tr "settings.delete_key"}}
|
||||
</button>
|
||||
</div>
|
||||
<i class="mega-octicon octicon-key {{if .HasRecentActivity}}green{{end}}" {{if .HasRecentActivity}}data-content="{{$.i18n.Tr "settings.key_state_desc"}}" data-variation="inverted tiny"{{end}}></i>
|
||||
<div class="content">
|
||||
<strong>{{.Name}}</strong>
|
||||
<div class="print meta">
|
||||
{{.Fingerprint}}
|
||||
</div>
|
||||
<div class="activity meta">
|
||||
<i>{{$.i18n.Tr "settings.add_on"}} <span>{{DateFmtShort .Created}}</span> — <i class="octicon octicon-info"></i> {{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{DateFmtShort .Updated}}</span>{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}}</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<p>{{.i18n.Tr "settings.ssh_helper" "https://help.github.com/articles/generating-ssh-keys" "https://help.github.com/ssh-issues/" | Str2html}}</p>
|
||||
<div {{if not .HasSSHError}}class="hide"{{end}} id="add-ssh-key-panel">
|
||||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "settings.add_new_key"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field {{if .Err_Title}}error{{end}}">
|
||||
<label for="title">{{.i18n.Tr "settings.key_name"}}</label>
|
||||
<input id="ssh-key-title" name="title" value="{{.title}}" autofocus required>
|
||||
</div>
|
||||
<div class="field {{if .Err_Content}}error{{end}}">
|
||||
<label for="content">{{.i18n.Tr "settings.key_content"}}</label>
|
||||
<textarea id="ssh-key-content" name="content" required>{{.content}}</textarea>
|
||||
</div>
|
||||
<input name="type" type="hidden" value="ssh">
|
||||
<button class="ui green button">
|
||||
{{.i18n.Tr "settings.add_key"}}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui small basic delete modal" id="delete-ssh">
|
||||
<div class="ui icon header">
|
||||
<i class="trash icon"></i>
|
||||
{{.i18n.Tr "settings.ssh_key_deletion"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{.i18n.Tr "settings.ssh_key_deletion_desc"}}</p>
|
||||
</div>
|
||||
{{template "base/delete_modal_actions" .}}
|
||||
</div>
|
|
@ -16,8 +16,8 @@
|
|||
OpenID
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsSSHKeys}}active{{end}} item" href="{{AppSubUrl}}/user/settings/ssh">
|
||||
{{.i18n.Tr "settings.ssh_keys"}}
|
||||
<a class="{{if .PageIsSettingsKeys}}active{{end}} item" href="{{AppSubUrl}}/user/settings/keys">
|
||||
{{.i18n.Tr "settings.ssh_gpg_keys"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsApplications}}active{{end}} item" href="{{AppSubUrl}}/user/settings/applications">
|
||||
{{.i18n.Tr "settings.applications"}}
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
{{template "base/head" .}}
|
||||
<div class="user settings sshkeys">
|
||||
{{template "user/settings/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "settings.manage_ssh_keys"}}
|
||||
<div class="ui right">
|
||||
<div class="ui blue tiny show-panel button" data-panel="#add-ssh-key-panel">{{.i18n.Tr "settings.add_key"}}</div>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<div class="ui key list">
|
||||
<div class="item">
|
||||
{{.i18n.Tr "settings.ssh_desc"}}
|
||||
</div>
|
||||
{{range .Keys}}
|
||||
<div class="item">
|
||||
<div class="right floated content">
|
||||
<button class="ui red tiny button delete-button" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
|
||||
{{$.i18n.Tr "settings.delete_key"}}
|
||||
</button>
|
||||
</div>
|
||||
<i class="mega-octicon octicon-key {{if .HasRecentActivity}}green{{end}}" {{if .HasRecentActivity}}data-content="{{$.i18n.Tr "settings.key_state_desc"}}" data-variation="inverted tiny"{{end}}></i>
|
||||
<div class="content">
|
||||
<strong>{{.Name}}</strong>
|
||||
<div class="print meta">
|
||||
{{.Fingerprint}}
|
||||
</div>
|
||||
<div class="activity meta">
|
||||
<i>{{$.i18n.Tr "settings.add_on"}} <span>{{DateFmtShort .Created}}</span> — <i class="octicon octicon-info"></i> {{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{DateFmtShort .Updated}}</span>{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}}</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<p>{{.i18n.Tr "settings.ssh_helper" "https://help.github.com/articles/generating-ssh-keys" "https://help.github.com/ssh-issues/" | Str2html}}</p>
|
||||
<div {{if not .HasError}}class="hide"{{end}} id="add-ssh-key-panel">
|
||||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "settings.add_new_key"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field {{if .Err_Title}}error{{end}}">
|
||||
<label for="title">{{.i18n.Tr "settings.key_name"}}</label>
|
||||
<input id="ssh-key-title" name="title" value="{{.title}}" autofocus required>
|
||||
</div>
|
||||
<div class="field {{if .Err_Content}}error{{end}}">
|
||||
<label for="content">{{.i18n.Tr "settings.key_content"}}</label>
|
||||
<textarea id="ssh-key-content" name="content" required>{{.content}}</textarea>
|
||||
</div>
|
||||
<button class="ui green button">
|
||||
{{.i18n.Tr "settings.add_key"}}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui small basic delete modal">
|
||||
<div class="ui icon header">
|
||||
<i class="trash icon"></i>
|
||||
{{.i18n.Tr "settings.ssh_key_deletion"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{.i18n.Tr "settings.ssh_key_deletion_desc"}}</p>
|
||||
</div>
|
||||
{{template "base/delete_modal_actions" .}}
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
Loading…
Reference in New Issue