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.
305 lines
12 KiB
Cheetah
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" .}}
|