Files

263 lines
8.4 KiB
HTML
Raw Permalink Normal View History

2025-09-03 16:32:44 +03:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Secrets</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
margin: 20px;
background: #f5f5f5;
color: #333;
}
h1 {
font-size: 18px;
margin-bottom: 20px;
}
.secret {
background: white;
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 10px;
border-radius: 4px;
position: relative;
}
.secret.expired {
border-color: #e74c3c;
background: #fdf2f2;
}
.secret-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
2025-09-03 16:32:44 +03:00
}
.secret-name {
font-weight: bold;
font-size: 16px;
}
.source-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
}
.source-badge.k8s {
background: #326ce5;
color: white;
}
.source-badge.webhook {
background: #6c5ce7;
color: white;
}
.expired-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
background: #e74c3c;
color: white;
}
2025-09-03 16:32:44 +03:00
.data-item {
display: flex;
align-items: center;
margin: 8px 0;
padding: 8px;
background: #f9f9f9;
border-radius: 3px;
}
.data-key {
display: inline-block;
min-width: 150px;
font-weight: bold;
flex-shrink: 0;
}
.data-value-wrapper {
flex-grow: 1;
display: flex;
align-items: center;
gap: 10px;
}
.data-value {
font-family: 'Courier New', monospace;
background: #fff;
padding: 6px 10px;
border: 1px solid #e0e0e0;
border-radius: 3px;
word-break: break-all;
flex-grow: 1;
}
.copy-btn {
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
padding: 6px 10px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
flex-shrink: 0;
}
.copy-btn:hover {
background: #e0e0e0;
}
.copy-btn.copied {
background: #4caf50;
color: white;
border-color: #4caf50;
}
.totp-code {
font-size: 24px;
font-weight: bold;
font-family: 'Courier New', monospace;
letter-spacing: 3px;
color: #007bff;
padding: 8px 12px;
background: #e8f4ff;
border: 1px solid #007bff;
border-radius: 4px;
}
.totp-timer {
font-size: 12px;
color: #666;
margin-left: 10px;
}
2025-12-29 02:06:23 +00:00
.received-at {
font-size: 11px;
color: #888;
margin-top: 5px;
font-style: italic;
}
2025-09-03 16:32:44 +03:00
.error {
background: #ffdddd;
color: #cc0000;
padding: 10px;
margin-bottom: 20px;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>Secrets</h1>
{% if let Some(err) = error %}
<div class="error">
Error: {{ err }}
</div>
{% else %}
{% for secret in secrets %}
<div class="secret{% if secret.expired %} expired{% endif %}">
<div class="secret-header">
<div class="secret-name">{{ secret.name }}</div>
{% match secret.source %}
{% when SecretSource::Kubernetes %}
<span class="source-badge k8s">☸ K8s</span>
{% when SecretSource::Webhook %}
<span class="source-badge webhook">⚡ Webhook</span>
{% endmatch %}
{% if secret.expired %}
{% if let Some(expires) = secret.expires_at %}
<span class="expired-badge">⚠ Expired at {{ expires }}</span>
{% endif %}
{% endif %}
</div>
2025-09-03 16:32:44 +03:00
{% for (key, value) in secret.data %}
<div class="data-item">
<span class="data-key">{{ key }}:</span>
<div class="data-value-wrapper">
{% if value.starts_with("otpauth://totp/") %}
<span class="totp-code" id="totp-{{ secret.name }}-{{ key }}">------</span>
<span class="totp-timer" id="timer-{{ secret.name }}-{{ key }}"></span>
<button class="copy-btn" onclick="copyTotp('{{ secret.name }}-{{ key }}')">📋</button>
{% else %}
<code class="data-value" id="value-{{ secret.name }}-{{ key }}">{{ value }}</code>
<button class="copy-btn" onclick="copyValue('{{ secret.name }}-{{ key }}', '{{ value|escape }}')">📋</button>
{% endif %}
</div>
</div>
{% endfor %}
2025-12-29 02:06:23 +00:00
{% if let Some(received) = secret.received_at %}
<div class="received-at">Received: {{ received }}{% if let Some(expires) = secret.expires_at %}{% if !secret.expired %} · Expires: {{ expires }}{% endif %}{% endif %}</div>
2025-12-29 02:06:23 +00:00
{% endif %}
2025-09-03 16:32:44 +03:00
</div>
{% endfor %}
{% endif %}
<script src="https://unpkg.com/otpauth@9/dist/otpauth.umd.min.js"></script>
<script>
const otpUrls = [];
const totpValues = {};
{% for secret in secrets %}
{% for (key, value) in secret.data %}
{% if value.starts_with("otpauth://totp/") %}
otpUrls.push({
id: 'totp-{{ secret.name }}-{{ key }}',
timerId: 'timer-{{ secret.name }}-{{ key }}',
url: {{ value|json|safe }},
key: '{{ secret.name }}-{{ key }}'
});
{% endif %}
{% endfor %}
{% endfor %}
function updateTOTP() {
const now = Date.now();
const timeRemaining = 30 - (Math.floor(now / 1000) % 30);
otpUrls.forEach(item => {
try {
const totp = OTPAuth.URI.parse(item.url);
const token = totp.generate();
totpValues[item.key] = token.toString().padStart(6, '0');
const formatted = totpValues[item.key].match(/.{1,3}/g).join(' ');
document.getElementById(item.id).textContent = formatted;
document.getElementById(item.timerId).textContent = timeRemaining + 's';
} catch (e) {
document.getElementById(item.id).textContent = 'ERROR';
console.error('Failed to generate TOTP:', e);
}
});
}
function copyValue(id, value) {
const decodedValue = value.replace(/&quot;/g, '"').replace(/&#x27;/g, "'").replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
navigator.clipboard.writeText(decodedValue).then(() => {
const btn = event.target;
btn.classList.add('copied');
btn.textContent = '✓';
setTimeout(() => {
btn.classList.remove('copied');
btn.textContent = '📋';
}, 2000);
});
}
function copyTotp(key) {
const code = totpValues[key];
if (code) {
navigator.clipboard.writeText(code).then(() => {
const btn = event.target;
btn.classList.add('copied');
btn.textContent = '✓';
setTimeout(() => {
btn.classList.remove('copied');
btn.textContent = '📋';
}, 2000);
});
}
}
if (otpUrls.length > 0) {
updateTOTP();
setInterval(updateTOTP, 1000);
}
</script>
</body>
</html>