2
0
Files
gitcaddy-vault/templates/repo/vault/view.tmpl
logikonline fcb720736b
All checks were successful
Build and Release / Tests (push) Successful in 1m11s
Build and Release / Lint (push) Successful in 1m33s
Build and Release / Create Release (push) Successful in 0s
feat(vault): add lockbox encryption mode to secrets
Added encryption_mode field to secrets supporting "standard" (server-side) and "lockbox" (client-side E2E) modes. Updated API to validate lockbox format (lockbox:v1:salt:ciphertext). Enhanced UI to display lock icons and badges for lockbox secrets. Lockbox secrets show locked state in web UI, requiring CLI/SDK for decryption.
2026-02-07 02:14:26 -05:00

305 lines
12 KiB
Cheetah

{{template "repo/vault/layout_head" (dict "ctxData" . "pageClass" "repository vault secret-view")}}
<div class="ui segment">
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4">
<div>
<h4 class="ui header tw-mb-0">
{{if eq .Secret.EncryptionMode "lockbox"}}
{{svg "octicon-lock" 20}} {{.Secret.Name}}
<span class="ui blue label" data-tooltip="End-to-end encrypted. Use CLI/SDK to decrypt.">{{svg "octicon-shield-lock" 12}} Lock-Box</span>
{{else}}
{{svg "octicon-key" 20}} {{.Secret.Name}}
{{end}}
{{if .Secret.IsDeleted}}
<span class="ui red label">{{ctx.Locale.Tr "vault.deleted"}}</span>
{{end}}
</h4>
{{if .Secret.Description}}
<p class="text grey tw-mt-2 tw-mb-0">{{.Secret.Description}}</p>
{{end}}
</div>
<div class="tw-flex tw-gap-2 tw-flex-shrink-0">
<a class="ui button" href="{{.RepoLink}}/vault/secrets/{{.Secret.Name}}/versions">
{{svg "octicon-history" 16}} {{ctx.Locale.Tr "vault.view_all_versions"}}
</a>
<a class="ui button" href="{{.RepoLink}}/vault">
{{svg "octicon-arrow-left" 16}} {{ctx.Locale.Tr "vault.back_to_list"}}
</a>
</div>
</div>
<div class="ui four column stackable grid tw-mt-4">
<div class="column">
<div class="ui segment tw-text-center">
<div class="tw-font-semibold tw-text-lg">v{{.Secret.CurrentVersion}}</div>
<div class="tw-text-secondary">{{ctx.Locale.Tr "vault.version"}}</div>
</div>
</div>
<div class="column">
<div class="ui segment tw-text-center">
<div class="tw-font-semibold tw-text-lg">{{.Secret.Type}}</div>
<div class="tw-text-secondary">{{ctx.Locale.Tr "vault.secret_type"}}</div>
</div>
</div>
<div class="column">
<div class="ui segment tw-text-center">
<div class="tw-font-semibold tw-text-lg">{{DateUtils.TimeSince .Secret.CreatedUnix}}</div>
<div class="tw-text-secondary">{{ctx.Locale.Tr "vault.created"}}</div>
</div>
</div>
<div class="column">
<div class="ui segment tw-text-center">
<div class="tw-font-semibold tw-text-lg">{{DateUtils.TimeSince .Secret.UpdatedUnix}}</div>
<div class="tw-text-secondary">{{ctx.Locale.Tr "vault.updated"}}</div>
</div>
</div>
</div>
</div>
{{if eq .Secret.EncryptionMode "lockbox"}}
<!-- Lockbox secret - show locked state -->
<div class="ui segment tw-text-center">
<div class="tw-py-8">
{{svg "octicon-lock" 64 "tw-text-blue-500"}}
<h3 class="ui header tw-mt-4">Lock-Box Protected</h3>
<p class="tw-text-secondary tw-max-w-md tw-mx-auto">
This secret is end-to-end encrypted. The server cannot decrypt it.
Use the CLI or SDK with your passphrase to access the value.
</p>
<div class="ui divider"></div>
<div class="ui secondary segment tw-text-left tw-max-w-lg tw-mx-auto">
<p class="tw-font-semibold tw-mb-2">CLI Usage:</p>
<code class="tw-block tw-p-2 tw-bg-gray-100 dark:tw-bg-gray-800 tw-rounded tw-font-mono tw-text-sm">
vault-resolve get --lockbox {{.Secret.Name}}
</code>
<p class="tw-font-semibold tw-mb-2 tw-mt-4">Go SDK:</p>
<code class="tw-block tw-p-2 tw-bg-gray-100 dark:tw-bg-gray-800 tw-rounded tw-font-mono tw-text-sm">
value, err := client.GetLockbox(ctx, "{{.Secret.Name}}", passphrase)
</code>
</div>
{{if .CanWrite}}
<div class="tw-mt-4">
{{if .Secret.IsDeleted}}
<button class="ui green button" type="button" onclick="document.getElementById('restore-form').submit();">
{{svg "octicon-history" 16}} {{ctx.Locale.Tr "vault.restore"}}
</button>
{{else}}
<button class="ui red button show-modal" type="button" data-modal="#delete-secret-modal">
{{svg "octicon-trash" 16}} {{ctx.Locale.Tr "vault.delete"}}
</button>
{{end}}
</div>
{{if .Secret.IsDeleted}}
<form id="restore-form" class="tw-hidden" action="{{.RepoLink}}/vault/secrets/{{.Secret.Name}}/restore" method="post">
{{.CsrfTokenHtml}}
</form>
{{else}}
<form id="delete-form" class="tw-hidden" action="{{.RepoLink}}/vault/secrets/{{.Secret.Name}}/delete" method="post">
{{.CsrfTokenHtml}}
</form>
{{end}}
{{end}}
</div>
</div>
{{else if .CanWrite}}
<!-- Editor view for users who can write -->
<div class="ui segment">
<h5 class="ui header">{{ctx.Locale.Tr "vault.edit_secret"}}</h5>
<form class="ui form" id="edit-secret-form" action="{{.RepoLink}}/vault/secrets/{{.Secret.Name}}" method="post">
{{.CsrfTokenHtml}}
<div class="field">
<label>{{ctx.Locale.Tr "vault.value"}}</label>
<textarea name="value" id="edit-value" required style="min-height: 100px; resize: vertical; font-family: monospace;">{{.SecretValue}}</textarea>
<small class="tw-text-secondary">{{ctx.Locale.Tr "vault.edit_hint"}}</small>
</div>
<div class="tw-flex tw-gap-4">
<div class="field tw-flex-1">
<label>{{ctx.Locale.Tr "vault.type"}}</label>
<select name="type" class="ui dropdown">
<option value="generic" {{if eq .Secret.Type "generic"}}selected{{end}}>{{ctx.Locale.Tr "vault.type_generic"}}</option>
<option value="password" {{if eq .Secret.Type "password"}}selected{{end}}>{{ctx.Locale.Tr "vault.type_password"}}</option>
<option value="api_key" {{if eq .Secret.Type "api_key"}}selected{{end}}>{{ctx.Locale.Tr "vault.type_api_key"}}</option>
<option value="certificate" {{if eq .Secret.Type "certificate"}}selected{{end}}>{{ctx.Locale.Tr "vault.type_certificate"}}</option>
<option value="ssh_key" {{if eq .Secret.Type "ssh_key"}}selected{{end}}>{{ctx.Locale.Tr "vault.type_ssh_key"}}</option>
<option value="file" {{if eq .Secret.Type "file"}}selected{{end}}>{{ctx.Locale.Tr "vault.type_file"}}</option>
</select>
</div>
<div class="field tw-flex-1">
<label>{{ctx.Locale.Tr "vault.version_comment"}}</label>
<input type="text" name="comment" placeholder="{{ctx.Locale.Tr "vault.version_comment_placeholder"}}">
</div>
</div>
<div class="tw-flex tw-justify-between tw-items-center tw-mt-4">
<div class="tw-flex tw-gap-2">
<button class="ui primary button" type="submit">
{{svg "octicon-check" 16}} {{ctx.Locale.Tr "vault.save"}}
</button>
<button class="ui button" type="button" id="copy-secret" data-tooltip="{{ctx.Locale.Tr "vault.copy_value"}}">
{{svg "octicon-copy" 16}} {{ctx.Locale.Tr "vault.copy"}}
</button>
</div>
{{if .Secret.IsDeleted}}
<button class="ui green button" type="button" onclick="document.getElementById('restore-form').submit();">
{{svg "octicon-history" 16}} {{ctx.Locale.Tr "vault.restore"}}
</button>
{{else}}
<button class="ui red button show-modal" type="button" data-modal="#delete-secret-modal">
{{svg "octicon-trash" 16}} {{ctx.Locale.Tr "vault.delete"}}
</button>
{{end}}
</div>
</form>
{{if .Secret.IsDeleted}}
<form id="restore-form" class="tw-hidden" action="{{.RepoLink}}/vault/secrets/{{.Secret.Name}}/restore" method="post">
{{.CsrfTokenHtml}}
</form>
{{else}}
<form id="delete-form" class="tw-hidden" action="{{.RepoLink}}/vault/secrets/{{.Secret.Name}}/delete" method="post">
{{.CsrfTokenHtml}}
</form>
{{end}}
</div>
<script>
(function() {
const editValue = document.getElementById('edit-value');
const btnCopy = document.getElementById('copy-secret');
btnCopy?.addEventListener('click', function() {
navigator.clipboard.writeText(editValue.value).then(function() {
const originalText = btnCopy.innerHTML;
btnCopy.innerHTML = '{{svg "octicon-check" 16}} {{ctx.Locale.Tr "vault.copied"}}';
setTimeout(function() {
btnCopy.innerHTML = originalText;
}, 2000);
});
});
})();
</script>
{{else if ne .Secret.EncryptionMode "lockbox"}}
<!-- Read-only view for users who cannot write (standard secrets only) -->
<div class="ui segment">
<div class="tw-flex tw-items-center tw-justify-between tw-mb-4">
<h5 class="ui header tw-mb-0">{{ctx.Locale.Tr "vault.secret_value"}}</h5>
<div class="ui tiny buttons">
<button class="ui button active" id="view-hidden" data-tooltip="{{ctx.Locale.Tr "vault.view_hidden"}}">
{{svg "octicon-eye-closed" 14}} {{ctx.Locale.Tr "vault.hidden"}}
</button>
<button class="ui button" id="view-raw" data-tooltip="{{ctx.Locale.Tr "vault.view_raw"}}">
{{svg "octicon-eye" 14}} {{ctx.Locale.Tr "vault.raw"}}
</button>
<button class="ui button" id="copy-secret" data-tooltip="{{ctx.Locale.Tr "vault.copy_value"}}">
{{svg "octicon-copy" 14}} {{ctx.Locale.Tr "vault.copy"}}
</button>
</div>
</div>
<div class="ui form">
<div class="field">
<!-- Hidden view (default) -->
<div id="secret-hidden" class="ui segment secondary tw-font-mono" style="min-height: 60px; max-height: 300px; overflow: auto;">
<span class="tw-text-secondary">••••••••••••••••••••••••••••••••</span>
</div>
<!-- Raw view (hidden by default) -->
<textarea id="secret-value" class="tw-hidden tw-font-mono" readonly style="min-height: 100px; max-height: 400px; resize: vertical; width: 100%;">{{.SecretValue}}</textarea>
</div>
</div>
</div>
<script>
(function() {
const hiddenView = document.getElementById('secret-hidden');
const rawView = document.getElementById('secret-value');
const btnHidden = document.getElementById('view-hidden');
const btnRaw = document.getElementById('view-raw');
const btnCopy = document.getElementById('copy-secret');
function showHidden() {
hiddenView.classList.remove('tw-hidden');
rawView.classList.add('tw-hidden');
btnHidden.classList.add('active');
btnRaw.classList.remove('active');
}
function showRaw() {
hiddenView.classList.add('tw-hidden');
rawView.classList.remove('tw-hidden');
btnHidden.classList.remove('active');
btnRaw.classList.add('active');
rawView.style.height = 'auto';
rawView.style.height = Math.min(Math.max(rawView.scrollHeight, 100), 400) + 'px';
}
btnHidden?.addEventListener('click', showHidden);
btnRaw?.addEventListener('click', showRaw);
btnCopy?.addEventListener('click', function() {
navigator.clipboard.writeText(rawView.value).then(function() {
const originalText = btnCopy.innerHTML;
btnCopy.innerHTML = '{{svg "octicon-check" 14}} {{ctx.Locale.Tr "vault.copied"}}';
setTimeout(function() {
btnCopy.innerHTML = originalText;
}, 2000);
});
});
})();
</script>
{{end}}
{{if .Versions}}
<div class="ui segment">
<h5 class="ui header">{{ctx.Locale.Tr "vault.version_history"}}</h5>
<table class="ui very basic striped table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "vault.version"}}</th>
<th>{{ctx.Locale.Tr "vault.comment"}}</th>
<th>{{ctx.Locale.Tr "vault.created"}}</th>
{{if .CanWrite}}
<th class="right aligned">{{ctx.Locale.Tr "actions"}}</th>
{{end}}
</tr>
</thead>
<tbody>
{{range .Versions}}
<tr{{if eq .Version $.Secret.CurrentVersion}} class="positive"{{end}}>
<td>
v{{.Version}}
{{if eq .Version $.Secret.CurrentVersion}}
<span class="ui green label">{{ctx.Locale.Tr "vault.current"}}</span>
{{end}}
</td>
<td>{{if .Comment}}{{.Comment}}{{else}}<span class="text grey">-</span>{{end}}</td>
<td>{{DateUtils.TimeSince .CreatedUnix}}</td>
{{if $.CanWrite}}
<td class="right aligned">
{{if ne .Version $.Secret.CurrentVersion}}
<form class="ui inline" action="{{$.RepoLink}}/vault/secrets/{{$.Secret.Name}}/rollback" method="post">
{{$.CsrfTokenHtml}}
<input type="hidden" name="version" value="{{.Version}}">
<button class="ui tiny button" type="submit">
{{svg "octicon-history" 14}} {{ctx.Locale.Tr "vault.rollback"}}
</button>
</form>
{{end}}
</td>
{{end}}
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{if not .Secret.IsDeleted}}
<div class="ui small modal" id="delete-secret-modal">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "vault.delete_secret"}}</div>
<div class="content">
<p>{{ctx.Locale.Tr "vault.confirm_delete"}}</p>
</div>
<div class="actions">
<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
<button class="ui red button" onclick="document.getElementById('delete-form').submit();">{{ctx.Locale.Tr "vault.delete"}}</button>
</div>
</div>
{{end}}
{{template "repo/vault/layout_footer" .}}