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 error
This commit is contained in:
Antoine GIRARD 2017-04-26 15:10:43 +02:00 committed by Lunny Xiao
parent b7da5a6cb7
commit 8371f94d06
14 changed files with 324 additions and 140 deletions

View File

@ -4,6 +4,8 @@
* BREAKING * BREAKING
* Password reset URL changed from `/user/forget_password` to `/user/forgot_password` * 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 ## [1.1.0](https://github.com/go-gitea/gitea/releases/tag/v1.1.0) - 2017-03-09

View File

@ -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) 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. // ErrGPGKeyNotExist represents a "GPGKeyNotExist" kind of error.
type ErrGPGKeyNotExist struct { type ErrGPGKeyNotExist struct {
ID int64 ID int64

View File

@ -89,7 +89,7 @@ func GetGPGKeyByID(keyID int64) (*GPGKey, error) {
func checkArmoredGPGKeyString(content string) (*openpgp.Entity, error) { func checkArmoredGPGKeyString(content string) (*openpgp.Entity, error) {
list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content)) list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content))
if err != nil { if err != nil {
return nil, err return nil, ErrGPGKeyParsing{err}
} }
return list[0], nil return list[0], nil
} }
@ -219,7 +219,7 @@ func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) {
} }
} }
if emails[n] == nil { 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++ n++
} }
@ -400,6 +400,7 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
} }
} }
for _, k := range keys {
//Generating hash of commit //Generating hash of commit
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload)) hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
if err != nil { //Skipping ailed to generate hash if err != nil { //Skipping ailed to generate hash
@ -409,8 +410,6 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
Reason: "gpg.error.generate_hash", Reason: "gpg.error.generate_hash",
} }
} }
for _, k := range keys {
//We get PK //We get PK
if err := verifySign(sig, hash, k); err == nil { if err := verifySign(sig, hash, k); err == nil {
return &CommitVerification{ //Everything is ok return &CommitVerification{ //Everything is ok
@ -422,6 +421,16 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
} }
//And test also SubsKey //And test also SubsKey
for _, sk := range k.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 { if err := verifySign(sig, hash, sk); err == nil {
return &CommitVerification{ //Everything is ok return &CommitVerification{ //Everything is ok
Verified: true, Verified: true,

View File

@ -163,14 +163,15 @@ func (f *AddOpenIDForm) Validate(ctx *macaron.Context, errs binding.Errors) bind
return validate(errs, ctx.Data, f, ctx.Locale) return validate(errs, ctx.Data, f, ctx.Locale)
} }
// AddSSHKeyForm form for adding SSH key // AddKeyForm form for adding SSH/GPG key
type AddSSHKeyForm struct { type AddKeyForm struct {
Type string `binding:"OmitEmpty"`
Title string `binding:"Required;MaxSize(50)"` Title string `binding:"Required;MaxSize(50)"`
Content string `binding:"Required"` Content string `binding:"Required"`
} }
// Validate validates the fields // 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) return validate(errs, ctx.Data, f, ctx.Locale)
} }

View File

@ -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. 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_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. 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 auth_failed = Authentication failed: %v
@ -285,7 +286,7 @@ form.name_pattern_not_allowed = Username pattern '%s' is not allowed.
profile = Profile profile = Profile
password = Password password = Password
avatar = Avatar avatar = Avatar
ssh_keys = SSH Keys ssh_gpg_keys = SSH / GPG Keys
social = Social Accounts social = Social Accounts
applications = Applications applications = Applications
orgs = Organizations 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 openid_desc = Your OpenID addresses will let you delegate authentication to your provider of choice
manage_ssh_keys = Manage SSH Keys manage_ssh_keys = Manage SSH Keys
manage_gpg_keys = Manage GPG Keys
add_key = Add Key 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. 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. 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_key = Add SSH Key
add_new_gpg_key = Add GPG Key
ssh_key_been_used = Public key content has already been used. ssh_key_been_used = Public key content has already been used.
ssh_key_name_used = Public key with same name already exists. 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_name = Key Name
key_content = Content key_content = Content
add_key_success = Your new SSH key '%s' has been added successfully! 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 delete_key = Delete
ssh_key_deletion = SSH Key Deletion 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? 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! ssh_key_deletion_success = SSH key has been deleted successfully!
gpg_key_deletion_success = GPG key has been deleted successfully!
add_on = Added on add_on = Added on
valid_until = Valid until
last_used = Last used on last_used = Last used on
no_activity = No recent activity no_activity = No recent activity
key_state_desc = This key is used in last 7 days 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.extract_sign = Failed to extract signature
error.generate_hash = Failed to generate hash of commit error.generate_hash = Failed to generate hash of commit
error.no_committer_account = No account linked to committer email 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.no_gpg_keys_found = "No known key found for this signature in database"
error.not_signed_commit = "Not a signed commit" error.not_signed_commit = "Not a signed commit"
error.failed_retrieval_gpg_keys = "Failed to retrieve any key attached to the commiter account" error.failed_retrieval_gpg_keys = "Failed to retrieve any key attached to the commiter account"

View File

@ -1408,7 +1408,11 @@ $(document).ready(function () {
// Helpers. // Helpers.
$('.delete-button').click(function () { $('.delete-button').click(function () {
var $this = $(this); var $this = $(this);
$('.delete.modal').modal({ var filter = "";
if ($this.attr("id")) {
filter += "#"+$this.attr("id")
}
$('.delete.modal'+filter).modal({
closable: false, closable: false,
onApprove: function () { onApprove: function () {
if ($this.data('type') == "form") { if ($this.data('type') == "form") {

View File

@ -664,7 +664,7 @@ func DeployKeys(ctx *context.Context) {
} }
// DeployKeysPost response for adding a deploy key of a repository // 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["Title"] = ctx.Tr("repo.settings.deploy_keys")
ctx.Data["PageIsSettingsKeys"] = true ctx.Data["PageIsSettingsKeys"] = true

View File

@ -214,9 +214,9 @@ func RegisterRoutes(m *macaron.Macaron) {
}) })
} }
m.Combo("/ssh").Get(user.SettingsSSHKeys). m.Combo("/keys").Get(user.SettingsKeys).
Post(bindIgnErr(auth.AddSSHKeyForm{}), user.SettingsSSHKeysPost) Post(bindIgnErr(auth.AddKeyForm{}), user.SettingsKeysPost)
m.Post("/ssh/delete", user.DeleteSSHKey) m.Post("/keys/delete", user.DeleteKey)
m.Combo("/applications").Get(user.SettingsApplications). m.Combo("/applications").Get(user.SettingsApplications).
Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost) Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
m.Post("/applications/delete", user.SettingsDeleteApplication) m.Post("/applications/delete", user.SettingsDeleteApplication)
@ -438,7 +438,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/keys", func() { m.Group("/keys", func() {
m.Combo("").Get(repo.DeployKeys). m.Combo("").Get(repo.DeployKeys).
Post(bindIgnErr(auth.AddSSHKeyForm{}), repo.DeployKeysPost) Post(bindIgnErr(auth.AddKeyForm{}), repo.DeployKeysPost)
m.Post("/delete", repo.DeleteDeployKey) m.Post("/delete", repo.DeleteDeployKey)
}) })

View File

@ -32,7 +32,7 @@ const (
tplSettingsAvatar base.TplName = "user/settings/avatar" tplSettingsAvatar base.TplName = "user/settings/avatar"
tplSettingsPassword base.TplName = "user/settings/password" tplSettingsPassword base.TplName = "user/settings/password"
tplSettingsEmails base.TplName = "user/settings/email" tplSettingsEmails base.TplName = "user/settings/email"
tplSettingsSSHKeys base.TplName = "user/settings/sshkeys" tplSettingsKeys base.TplName = "user/settings/keys"
tplSettingsSocial base.TplName = "user/settings/social" tplSettingsSocial base.TplName = "user/settings/social"
tplSettingsApplications base.TplName = "user/settings/applications" tplSettingsApplications base.TplName = "user/settings/applications"
tplSettingsTwofa base.TplName = "user/settings/twofa" tplSettingsTwofa base.TplName = "user/settings/twofa"
@ -320,10 +320,10 @@ func DeleteEmail(ctx *context.Context) {
}) })
} }
// SettingsSSHKeys render user's SSH public keys page // SettingsKeys render user's SSH/GPG public keys page
func SettingsSSHKeys(ctx *context.Context) { func SettingsKeys(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSSHKeys"] = true ctx.Data["PageIsSettingsKeys"] = true
keys, err := models.ListPublicKeys(ctx.User.ID) keys, err := models.ListPublicKeys(ctx.User.ID)
if err != nil { if err != nil {
@ -332,13 +332,20 @@ func SettingsSSHKeys(ctx *context.Context) {
} }
ctx.Data["Keys"] = keys 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 // SettingsKeysPost response for change user's SSH/GPG keys
func SettingsSSHKeysPost(ctx *context.Context, form auth.AddSSHKeyForm) { func SettingsKeysPost(ctx *context.Context, form auth.AddKeyForm) {
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSSHKeys"] = true ctx.Data["PageIsSettingsKeys"] = true
keys, err := models.ListPublicKeys(ctx.User.ID) keys, err := models.ListPublicKeys(ctx.User.ID)
if err != nil { if err != nil {
@ -347,51 +354,97 @@ func SettingsSSHKeysPost(ctx *context.Context, form auth.AddSSHKeyForm) {
} }
ctx.Data["Keys"] = keys 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() { if ctx.HasError() {
ctx.HTML(200, tplSettingsSSHKeys) ctx.HTML(200, tplSettingsKeys)
return return
} }
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
}
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) content, err := models.CheckPublicKeyString(form.Content)
if err != nil { if err != nil {
if models.IsErrKeyUnableVerify(err) { if models.IsErrKeyUnableVerify(err) {
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
} else { } else {
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error())) ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
ctx.Redirect(setting.AppSubURL + "/user/settings/ssh") ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
return return
} }
} }
if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil { if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil {
ctx.Data["HasError"] = true ctx.Data["HasSSHError"] = true
switch { switch {
case models.IsErrKeyAlreadyExist(err): case models.IsErrKeyAlreadyExist(err):
ctx.Data["Err_Content"] = true ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsSSHKeys, &form) ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
case models.IsErrKeyNameAlreadyUsed(err): case models.IsErrKeyNameAlreadyUsed(err):
ctx.Data["Err_Title"] = true ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsSSHKeys, &form) ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
default: default:
ctx.Handle(500, "AddPublicKey", err) ctx.Handle(500, "AddPublicKey", err)
} }
return return
} }
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title)) ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
ctx.Redirect(setting.AppSubURL + "/user/settings/ssh") ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
default:
ctx.Flash.Warning("Function not implemented")
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
} }
// DeleteSSHKey response for delete user's SSH key }
func DeleteSSHKey(ctx *context.Context) {
// 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 { if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeletePublicKey: " + err.Error()) ctx.Flash.Error("DeletePublicKey: " + err.Error())
} else { } else {
ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success")) 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{}{ ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/ssh", "redirect": setting.AppSubURL + "/user/settings/keys",
}) })
} }

View File

@ -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" .}}

View File

@ -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>

View File

@ -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>

View File

@ -16,8 +16,8 @@
OpenID OpenID
</a> </a>
{{end}} {{end}}
<a class="{{if .PageIsSettingsSSHKeys}}active{{end}} item" href="{{AppSubUrl}}/user/settings/ssh"> <a class="{{if .PageIsSettingsKeys}}active{{end}} item" href="{{AppSubUrl}}/user/settings/keys">
{{.i18n.Tr "settings.ssh_keys"}} {{.i18n.Tr "settings.ssh_gpg_keys"}}
</a> </a>
<a class="{{if .PageIsSettingsApplications}}active{{end}} item" href="{{AppSubUrl}}/user/settings/applications"> <a class="{{if .PageIsSettingsApplications}}active{{end}} item" href="{{AppSubUrl}}/user/settings/applications">
{{.i18n.Tr "settings.applications"}} {{.i18n.Tr "settings.applications"}}

View File

@ -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" .}}