Add user secrets (#22191)

Fixes #22183
Replaces #22187

This PR adds secrets for users. I refactored the files for organizations
and repos to use the same logic and templates. I splitted the secrets
from deploy keys again and reverted the fix from #22187.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
KN4CK3R 2023-02-01 13:53:04 +01:00 committed by GitHub
parent 9f9a1ce922
commit 5882e179a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 310 additions and 255 deletions

View file

@ -1,6 +1,6 @@
---
date: "2022-12-19T21:26:00+08:00"
title: "Encrypted secrets"
title: "Secrets"
slug: "secrets/overview"
draft: false
toc: false
@ -12,24 +12,24 @@ menu:
identifier: "overview"
---
# Encrypted secrets
# Secrets
Encrypted secrets allow you to store sensitive information in your organization or repository.
Secrets allow you to store sensitive information in your user, organization or repository.
Secrets are available on Gitea 1.19+.
# Naming your secrets
The following rules apply to secret names:
Secret names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed.
- Secret names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed.
Secret names must not start with the `GITHUB_` and `GITEA_` prefix.
- Secret names must not start with the `GITHUB_` and `GITEA_` prefix.
Secret names must not start with a number.
- Secret names must not start with a number.
Secret names are not case-sensitive.
- Secret names are not case-sensitive.
Secret names must be unique at the level they are created at.
- Secret names must be unique at the level they are created at.
For example, a secret created at the repository level must have a unique name in that repository, and a secret created at the organization level must have a unique name at that level.

View file

@ -3254,7 +3254,7 @@ creation.value_placeholder = Input any content. Whitespace at the start and end
creation.success = The secret '%s' has been added.
creation.failed = Failed to add secret.
deletion = Remove secret
deletion.description = Removing a secret will revoke its access to repositories. Continue?
deletion.description = Removing a secret is permanent and cannot be undone. Continue?
deletion.success = The secret has been removed.
deletion.failed = Failed to remove secret.

View file

@ -12,7 +12,6 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
@ -38,8 +37,6 @@ const (
tplSettingsHooks base.TplName = "org/settings/hooks"
// tplSettingsLabels template path for render labels settings
tplSettingsLabels base.TplName = "org/settings/labels"
// tplSettingsSecrets template path for render secrets settings
tplSettingsSecrets base.TplName = "org/settings/secrets"
// tplSettingsRunners template path for render runners settings
tplSettingsRunners base.TplName = "org/settings/runners"
// tplSettingsRunnersEdit template path for render runners edit settings
@ -253,51 +250,3 @@ func Labels(ctx *context.Context) {
ctx.Data["LabelTemplates"] = repo_module.LabelTemplates
ctx.HTML(http.StatusOK, tplSettingsLabels)
}
// Secrets render organization secrets page
func Secrets(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.secrets")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsOrgSettingsSecrets"] = true
secrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{OwnerID: ctx.Org.Organization.ID})
if err != nil {
ctx.ServerError("FindSecrets", err)
return
}
ctx.Data["Secrets"] = secrets
ctx.HTML(http.StatusOK, tplSettingsSecrets)
}
// SecretsPost add secrets
func SecretsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AddSecretForm)
_, err := secret_model.InsertEncryptedSecret(ctx, ctx.Org.Organization.ID, 0, form.Title, form.Content)
if err != nil {
ctx.Flash.Error(ctx.Tr("secrets.creation.failed"))
log.Error("validate secret: %v", err)
ctx.Redirect(ctx.Org.OrgLink + "/settings/secrets")
return
}
log.Trace("Org %d: secret added", ctx.Org.Organization.ID)
ctx.Flash.Success(ctx.Tr("secrets.creation.success", form.Title))
ctx.Redirect(ctx.Org.OrgLink + "/settings/secrets")
}
// SecretsDelete delete secrets
func SecretsDelete(ctx *context.Context) {
id := ctx.FormInt64("id")
if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id}); err != nil {
ctx.Flash.Error(ctx.Tr("secrets.deletion.failed"))
log.Error("delete secret %d: %v", id, err)
} else {
ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Org.OrgLink + "/settings/secrets",
})
}

View file

@ -0,0 +1,48 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
shared "code.gitea.io/gitea/routers/web/shared/secrets"
)
const (
tplSettingsSecrets base.TplName = "org/settings/secrets"
)
// Secrets render organization secrets page
func Secrets(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("secrets.secrets")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsOrgSettingsSecrets"] = true
shared.SetSecretsContext(ctx, ctx.ContextUser.ID, 0)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplSettingsSecrets)
}
// SecretsPost add secrets
func SecretsPost(ctx *context.Context) {
shared.PerformSecretsPost(
ctx,
ctx.ContextUser.ID,
0,
ctx.Org.OrgLink+"/settings/secrets",
)
}
// SecretsDelete delete secrets
func SecretsDelete(ctx *context.Context) {
shared.PerformSecretsDelete(
ctx,
ctx.Org.OrgLink+"/settings/secrets",
)
}

View file

@ -19,7 +19,6 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
@ -1131,33 +1130,9 @@ func DeployKeys(ctx *context.Context) {
}
ctx.Data["Deploykeys"] = keys
secrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{RepoID: ctx.Repo.Repository.ID})
if err != nil {
ctx.ServerError("FindSecrets", err)
return
}
ctx.Data["Secrets"] = secrets
ctx.HTML(http.StatusOK, tplDeployKeys)
}
// SecretsPost response for creating a new secret
func SecretsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AddSecretForm)
_, err := secret_model.InsertEncryptedSecret(ctx, 0, ctx.Repo.Repository.ID, form.Title, form.Content)
if err != nil {
ctx.Flash.Error(ctx.Tr("secrets.creation.failed"))
log.Error("validate secret: %v", err)
ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
return
}
log.Trace("Secret added: %d", ctx.Repo.Repository.ID)
ctx.Flash.Success(ctx.Tr("secrets.creation.success", form.Title))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
}
// DeployKeysPost response for adding a deploy key of a repository
func DeployKeysPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AddKeyForm)
@ -1219,20 +1194,6 @@ func DeployKeysPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
}
func DeleteSecret(ctx *context.Context) {
id := ctx.FormInt64("id")
if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id}); err != nil {
ctx.Flash.Error(ctx.Tr("secrets.deletion.failed"))
log.Error("delete secret %d: %v", id, err)
} else {
ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/settings/keys",
})
}
// DeleteDeployKey response for deleting a deploy key
func DeleteDeployKey(ctx *context.Context) {
if err := asymkey_service.DeleteDeployKey(ctx.Doer, ctx.FormInt64("id")); err != nil {

View file

@ -0,0 +1,46 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
shared "code.gitea.io/gitea/routers/web/shared/secrets"
)
const (
tplSecrets base.TplName = "repo/settings/secrets"
)
func Secrets(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("secrets.secrets")
ctx.Data["PageIsSettingsSecrets"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
shared.SetSecretsContext(ctx, 0, ctx.Repo.Repository.ID)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplSecrets)
}
func SecretsPost(ctx *context.Context) {
shared.PerformSecretsPost(
ctx,
0,
ctx.Repo.Repository.ID,
ctx.Repo.RepoLink+"/settings/secrets",
)
}
func DeleteSecret(ctx *context.Context) {
shared.PerformSecretsDelete(
ctx,
ctx.Repo.RepoLink+"/settings/secrets",
)
}

View file

@ -0,0 +1,54 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package secrets
import (
"net/http"
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
secrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{OwnerID: ownerID, RepoID: repoID})
if err != nil {
ctx.ServerError("FindSecrets", err)
return
}
ctx.Data["Secrets"] = secrets
}
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.AddSecretForm)
s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Title, form.Content)
if err != nil {
log.Error("InsertEncryptedSecret: %v", err)
ctx.Flash.Error(ctx.Tr("secrets.creation.failed"))
} else {
ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name))
}
ctx.Redirect(redirectURL)
}
func PerformSecretsDelete(ctx *context.Context, redirectURL string) {
id := ctx.FormInt64("id")
if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id}); err != nil {
log.Error("Delete secret %d failed: %v", id, err)
ctx.Flash.Error(ctx.Tr("secrets.deletion.failed"))
} else {
ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": redirectURL,
})
}

View file

@ -0,0 +1,45 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
shared "code.gitea.io/gitea/routers/web/shared/secrets"
)
const (
tplSettingsSecrets base.TplName = "user/settings/secrets"
)
func Secrets(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("secrets.secrets")
ctx.Data["PageIsSettingsSecrets"] = true
shared.SetSecretsContext(ctx, ctx.Doer.ID, 0)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplSettingsSecrets)
}
func SecretsPost(ctx *context.Context) {
shared.PerformSecretsPost(
ctx,
ctx.Doer.ID,
0,
setting.AppSubURL+"/user/settings/secrets",
)
}
func SecretsDelete(ctx *context.Context) {
shared.PerformSecretsDelete(
ctx,
setting.AppSubURL+"/user/settings/secrets",
)
}

View file

@ -469,6 +469,11 @@ func RegisterRoutes(m *web.Route) {
})
})
}, packagesEnabled)
m.Group("/secrets", func() {
m.Get("", user_setting.Secrets)
m.Post("", web.Bind(forms.AddSecretForm{}), user_setting.SecretsPost)
m.Post("/delete", user_setting.SecretsDelete)
})
m.Get("/organization", user_setting.Organization)
m.Get("/repos", user_setting.Repos)
m.Post("/repos/unadopted", user_setting.AdoptOrDeleteRepository)
@ -982,11 +987,13 @@ func RegisterRoutes(m *web.Route) {
m.Combo("").Get(repo.DeployKeys).
Post(web.Bind(forms.AddKeyForm{}), repo.DeployKeysPost)
m.Post("/delete", repo.DeleteDeployKey)
})
m.Group("/secrets", func() {
m.Get("", repo.Secrets)
m.Post("", web.Bind(forms.AddSecretForm{}), repo.SecretsPost)
m.Post("/delete", repo.DeleteSecret)
})
})
m.Group("/lfs", func() {
m.Get("/", repo.LFSFiles)

View file

@ -6,78 +6,10 @@
{{template "org/settings/navbar" .}}
<div class="ui twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.locale.Tr "secrets.secrets"}}
<div class="ui right">
<div class="ui primary tiny show-panel button" data-panel="#add-secret-panel">{{.locale.Tr "secrets.creation"}}</div>
</div>
</h4>
<div class="ui attached segment">
<div class="{{if not .HasError}}hide {{end}}mb-4" id="add-secret-panel">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="field">
{{.locale.Tr "secrets.description"}}
</div>
<div class="field{{if .Err_Title}} error{{end}}">
<label for="secret-title">{{.locale.Tr "secrets.name"}}</label>
<input id="secret-title" name="title" value="{{.title}}" autofocus required pattern="^[a-zA-Z_][a-zA-Z0-9_]*$" placeholder="{{.locale.Tr "secrets.creation.name_placeholder"}}">
</div>
<div class="field{{if .Err_Content}} error{{end}}">
<label for="secret-content">{{.locale.Tr "secrets.value"}}</label>
<textarea id="secret-content" name="content" required placeholder="{{.locale.Tr "secrets.creation.value_placeholder"}}">{{.content}}</textarea>
</div>
<button class="ui green button">
{{.locale.Tr "secrets.creation"}}
</button>
<button class="ui hide-panel button" data-panel="#add-secret-panel">
{{.locale.Tr "cancel"}}
</button>
</form>
</div>
{{if .Secrets}}
<div class="ui key list">
{{range .Secrets}}
<div class="item">
<div class="right floated content">
<button class="ui red tiny button delete-button" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
{{$.locale.Tr "settings.delete_key"}}
</button>
</div>
<div class="left floated content">
<i>{{svg "octicon-key" 32}}</i>
</div>
<div class="content">
<strong>{{.Name}}</strong>
<div class="print meta">******</div>
<div class="activity meta">
<i>
{{$.locale.Tr "settings.add_on"}}
<span>{{.CreatedUnix.FormatShort}}</span>
</i>
{{template "shared/secrets/add_list" .}}
</div>
</div>
</div>
{{end}}
</div>
{{else}}
{{.locale.Tr "secrets.none"}}
{{end}}
</div>
</div>
</div>
</div>
</div>
<div class="ui small basic delete modal">
<div class="ui header">
{{svg "octicon-trash" 16 "mr-2"}}
{{.locale.Tr "secrets.deletion"}}
</div>
<div class="content">
<p>{{.locale.Tr "secrets.deletion.description"}}</p>
</div>
{{template "base/delete_modal_actions" .}}
</div>
{{template "base/footer" .}}

View file

@ -51,7 +51,7 @@
{{range .Deploykeys}}
<div class="item">
<div class="right floated content">
<button class="ui red tiny button delete-button" data-modal-id="delete-deploy_keys-modal" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
<button class="ui red tiny button delete-button" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
{{$.locale.Tr "settings.delete_key"}}
</button>
</div>
@ -75,11 +75,9 @@
{{end}}
</div>
</div>
<br/>
{{template "repo/settings/secrets" .}}
</div>
<div class="ui small basic delete modal" id="delete-deploy_keys-modal">
<div class="ui small basic delete modal">
<div class="ui icon header">
{{svg "octicon-trash"}}
{{.locale.Tr "repo.settings.deploy_key_deletion"}}

View file

@ -12,7 +12,8 @@
{{if or .SignedUser.AllowGitHook .SignedUser.IsAdmin}}
<li {{if .PageIsSettingsGitHooks}}class="current"{{end}}><a href="{{.RepoLink}}/settings/hooks/git">{{.locale.Tr "repo.settings.githooks"}}</a></li>
{{end}}
<li {{if .PageIsSettingsKeys}}class="current"{{end}}><a href="{{.RepoLink}}/settings/keys">{{.locale.Tr "secrets.secrets"}}</a></li>
<li {{if .PageIsSettingsKeys}}class="current"{{end}}><a href="{{.RepoLink}}/settings/keys">{{.locale.Tr "repo.settings.deploy_keys"}}</a></li>
<li {{if .PageIsSettingsSecrets}}class="current"{{end}}><a href="{{.RepoLink}}/settings/secrets">{{.locale.Tr "secrets.secrets"}}</a></li>
</ul>
</div>
</div>

View file

@ -25,6 +25,9 @@
</a>
{{end}}
<a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{.RepoLink}}/settings/keys">
{{.locale.Tr "repo.settings.deploy_keys"}}
</a>
<a class="{{if .PageIsSettingsSecrets}}active {{end}}item" href="{{.RepoLink}}/settings/secrets">
{{.locale.Tr "secrets.secrets"}}
</a>
{{if .LFSStartServer}}

View file

@ -1,80 +1,10 @@
{{template "base/head" .}}
<div class="page-content repository settings">
{{template "repo/header" .}}
{{template "repo/settings/navbar" .}}
<div class="ui container">
<h4 class="ui top attached header">
{{.locale.Tr "secrets.secrets"}}
<div class="ui right">
<div class="ui primary tiny show-panel button" data-panel="#add-secret-panel">{{.locale.Tr "secrets.creation"}}</div>
</div>
</h4>
<div class="ui attached segment">
<div class="{{if not .HasError}}hide {{end}}mb-4" id="add-secret-panel">
<form class="ui form" action="{{.Link}}/secrets" method="post">
{{.CsrfTokenHtml}}
<div class="field">
{{.locale.Tr "secrets.description"}}
</div>
<div class="field{{if .Err_Title}} error{{end}}">
<label for="secret-title">{{.locale.Tr "secrets.name"}}</label>
<input id="secret-title" name="title" value="{{.title}}" autofocus required pattern="^[a-zA-Z_][a-zA-Z0-9_]*$" placeholder="{{.locale.Tr "secrets.creation.name_placeholder"}}">
</div>
<div class="field{{if .Err_Content}} error{{end}}">
<label for="secret-content">{{.locale.Tr "secrets.value"}}</label>
<textarea id="secret-content" name="content" required placeholder="{{.locale.Tr "secrets.creation.value_placeholder"}}">{{.content}}</textarea>
</div>
<button class="ui green button">
{{.locale.Tr "secrets.creation"}}
</button>
<button class="ui hide-panel button" data-panel="#add-secret-panel">
{{.locale.Tr "cancel"}}
</button>
</form>
</div>
{{if .Secrets}}
<div class="ui key list">
{{range .Secrets}}
<div class="item">
<div class="right floated content">
<button class="ui red tiny button delete-button" data-modal-id="delete-secret-modal" data-url="{{$.Link}}/secrets/delete" data-id="{{.ID}}">
{{$.locale.Tr "settings.delete_key"}}
</button>
</div>
<div class="left floated content">
<i>{{svg "octicon-key" 32}}</i>
</div>
<div class="content">
<strong>{{.Name}}</strong>
<div class="print meta">******</div>
<div class="activity meta">
<i>
{{$.locale.Tr "settings.add_on"}}
<span>{{.CreatedUnix.FormatShort}}</span>
</i>
</div>
</div>
</div>
{{end}}
</div>
{{else}}
{{.locale.Tr "secrets.none"}}
{{end}}
</div>
</div>
<div class="ui small basic delete modal" id="delete-secret-modal">
<div class="ui icon header">
{{svg "octicon-trash"}}
{{.locale.Tr "secrets.deletion"}}
</div>
<div class="content">
<p>{{.locale.Tr "secrets.deletion.description"}}</p>
</div>
<div class="actions">
<div class="ui red basic inverted cancel button">
<i class="remove icon"></i>
{{.locale.Tr "modal.no"}}
</div>
<div class="ui green basic inverted ok button">
<i class="checkmark icon"></i>
{{.locale.Tr "modal.yes"}}
</div>
{{template "base/alert" .}}
{{template "shared/secrets/add_list" .}}
</div>
</div>
{{template "base/footer" .}}

View file

@ -0,0 +1,68 @@
<h4 class="ui top attached header">
{{.locale.Tr "secrets.secrets"}}
<div class="ui right">
<div class="ui primary tiny show-panel button" data-panel="#add-secret-panel">{{.locale.Tr "secrets.creation"}}</div>
</div>
</h4>
<div class="ui attached segment">
<div class="{{if not .HasError}}hide {{end}}mb-4" id="add-secret-panel">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="field">
{{.locale.Tr "secrets.description"}}
</div>
<div class="field{{if .Err_Title}} error{{end}}">
<label for="secret-title">{{.locale.Tr "secrets.name"}}</label>
<input id="secret-title" name="title" value="{{.title}}" autofocus required pattern="^[a-zA-Z_][a-zA-Z0-9_]*$" placeholder="{{.locale.Tr "secrets.creation.name_placeholder"}}">
</div>
<div class="field{{if .Err_Content}} error{{end}}">
<label for="secret-content">{{.locale.Tr "secrets.value"}}</label>
<textarea id="secret-content" name="content" required placeholder="{{.locale.Tr "secrets.creation.value_placeholder"}}">{{.content}}</textarea>
</div>
<button class="ui green button">
{{.locale.Tr "secrets.creation"}}
</button>
<button class="ui hide-panel button" data-panel="#add-secret-panel">
{{.locale.Tr "cancel"}}
</button>
</form>
</div>
{{if .Secrets}}
<div class="ui key list">
{{range .Secrets}}
<div class="item">
<div class="right floated content">
<button class="ui red tiny button delete-button" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
{{$.locale.Tr "settings.delete_key"}}
</button>
</div>
<div class="left floated content">
<i>{{svg "octicon-key" 32}}</i>
</div>
<div class="content">
<strong>{{.Name}}</strong>
<div class="print meta">******</div>
<div class="activity meta">
<i>
{{$.locale.Tr "settings.add_on"}}
<span>{{.CreatedUnix.FormatShort}}</span>
</i>
</div>
</div>
</div>
{{end}}
</div>
{{else}}
{{.locale.Tr "secrets.none"}}
{{end}}
</div>
<div class="ui small basic delete modal">
<div class="ui header">
{{svg "octicon-trash" 16 "mr-2"}}
{{.locale.Tr "secrets.deletion"}}
</div>
<div class="content">
<p>{{.locale.Tr "secrets.deletion.description"}}</p>
</div>
{{template "base/delete_modal_actions" .}}
</div>

View file

@ -18,6 +18,9 @@
<a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{AppSubUrl}}/user/settings/keys">
{{.locale.Tr "settings.ssh_gpg_keys"}}
</a>
<a class="{{if .PageIsSettingsSecrets}}active {{end}}item" href="{{AppSubUrl}}/user/settings/secrets">
{{.locale.Tr "secrets.secrets"}}
</a>
{{if .EnablePackages}}
<a class="{{if .PageIsSettingsPackages}}active {{end}}item" href="{{AppSubUrl}}/user/settings/packages">
{{.locale.Tr "packages.title"}}

View file

@ -0,0 +1,10 @@
{{template "base/head" .}}
<div class="page-content user settings secrets">
{{template "user/settings/navbar" .}}
<div class="ui container">
{{template "base/alert" .}}
{{template "shared/secrets/add_list" .}}
</div>
</div>
{{template "base/footer" .}}