You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

671 lines
32 KiB

package app
import (
"fmt"
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
)
templ ServiceAccounts(data dash.ServiceAccountsData) {
<div class="container-fluid">
<!-- Page Header -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800">
<i class="fas fa-robot me-2"></i>Service Accounts
</h1>
<p class="mb-0 text-muted">Manage application credentials for automated processes</p>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#createServiceAccountModal">
<i class="fas fa-plus me-1"></i>Create Service Account
</button>
</div>
</div>
<!-- Summary Cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Total Service Accounts
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{fmt.Sprintf("%d", data.TotalAccounts)}
</div>
</div>
<div class="col-auto">
<i class="fas fa-id-card fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Active Accounts
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{fmt.Sprintf("%d", data.ActiveAccounts)}
</div>
</div>
<div class="col-auto">
<i class="fas fa-check-circle fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Last Updated
</div>
<div class="h6 mb-0 font-weight-bold text-gray-800">
{data.LastUpdated.Format("15:04")}
</div>
</div>
<div class="col-auto">
<i class="fas fa-clock fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Service Accounts Table -->
<div class="row">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-robot me-2"></i>Service Accounts
</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" width="100%" cellspacing="0" id="serviceAccountsTable">
<thead>
<tr>
<th>ID</th>
<th>Parent User</th>
<th>Access Key</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
for _, sa := range data.ServiceAccounts {
<tr>
<td>
<div class="d-flex align-items-center">
<i class="fas fa-robot me-2 text-muted"></i>
<code>{sa.ID}</code>
</div>
</td>
<td>
<i class="fas fa-user me-1 text-muted"></i>
{sa.ParentUser}
</td>
<td>
<code class="text-muted">{sa.AccessKeyId}</code>
</td>
<td>
if sa.Status == "Active" {
<span class="badge bg-success">Active</span>
} else {
<span class="badge bg-secondary">Inactive</span>
}
</td>
<td>{sa.CreateDate.Format("2006-01-02")}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-info"
data-action="show-sa-details" data-sa-id={ sa.ID }>
<i class="fas fa-info-circle"></i>
</button>
<button type="button" class="btn btn-outline-primary"
data-action="toggle-sa-status" data-sa-id={ sa.ID } data-current-status={ sa.Status }>
if sa.Status == "Active" {
<i class="fas fa-pause"></i>
} else {
<i class="fas fa-play"></i>
}
</button>
<button type="button" class="btn btn-outline-danger"
data-action="delete-sa" data-sa-id={ sa.ID }>
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
}
if len(data.ServiceAccounts) == 0 {
<tr>
<td colspan="6" class="text-center text-muted py-4">
<i class="fas fa-robot fa-3x mb-3 text-muted"></i>
<div>
<h5>No service accounts found</h5>
<p>Create your first service account for automated processes.</p>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Last Updated -->
<div class="row">
<div class="col-12">
<small class="text-muted">
<i class="fas fa-clock me-1"></i>
Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")}
</small>
</div>
</div>
</div>
<!-- Create Service Account Modal -->
<div class="modal fade" id="createServiceAccountModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-plus me-2"></i>Create Service Account
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createSAForm">
<div class="mb-3">
<label for="parentUser" class="form-label">Parent User *</label>
<select class="form-select" id="parentUser" name="parent_user" required>
<option value="">-- Select a user --</option>
for _, user := range data.AvailableUsers {
<option value={ user }>{ user }</option>
}
</select>
<small class="form-text text-muted">The service account will inherit permissions from this user</small>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="2"
placeholder="What is this service account used for?"></textarea>
</div>
<div class="mb-3">
<label for="expiration" class="form-label">Expiration (optional)</label>
<input type="datetime-local" class="form-control" id="expiration" name="expiration">
<small class="form-text text-muted">Leave empty for no expiration</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="handleCreateServiceAccount()">Create</button>
</div>
</div>
</div>
</div>
<!-- Service Account Details Modal -->
<div class="modal fade" id="saDetailsModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-robot me-2"></i>Service Account Details
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="saDetailsContent">
<!-- Content will be loaded dynamically -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Credentials Display Modal -->
<div class="modal fade" id="credentialsModal" tabindex="-1" role="dialog" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title">
<i class="fas fa-check-circle me-2"></i>Service Account Created Successfully
</h5>
</div>
<div class="modal-body">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Important:</strong> This is the only time you will see the secret access key. Please save it securely.
</div>
<div class="mb-4">
<h6 class="text-muted mb-3">AWS CLI Configuration</h6>
<p class="text-muted small">Use these credentials to configure AWS CLI or SDKs:</p>
<div class="mb-3">
<label class="form-label fw-bold">AWS_ACCESS_KEY_ID</label>
<div class="input-group">
<input type="text" class="form-control font-monospace" id="displayAccessKey" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="copyCredentialToClipboard(this, 'displayAccessKey')">
<i class="fas fa-copy"></i> Copy
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">AWS_SECRET_ACCESS_KEY</label>
<div class="input-group">
<input type="text" class="form-control font-monospace" id="displaySecretKey" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="copyCredentialToClipboard(this, 'displaySecretKey')">
<i class="fas fa-copy"></i> Copy
</button>
</div>
</div>
</div>
<div class="bg-light p-3 rounded">
<h6 class="text-muted mb-2">Example AWS CLI Usage:</h6>
<div class="font-monospace small">
<div>export AWS_ACCESS_KEY_ID=<span id="exampleAccessKey"></span></div>
<div>export AWS_SECRET_ACCESS_KEY=<span id="exampleSecretKey"></span></div>
<div>export AWS_ENDPOINT_URL=http://localhost:8333</div>
<div class="mt-2"># List buckets</div>
<div>aws s3 ls</div>
<div class="mt-2"># Upload a file</div>
<div>aws s3 cp myfile.txt s3://mybucket/</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="closeCredentialsModal()">
<i class="fas fa-check me-1"></i>I have saved the credentials
</button>
</div>
</div>
</div>
</div>
<!-- JavaScript for service account management -->
<script>
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('click', function(e) {
const button = e.target.closest('[data-action]');
if (!button) return;
const action = button.getAttribute('data-action');
const saId = button.getAttribute('data-sa-id');
switch (action) {
case 'show-sa-details':
showSADetails(saId);
break;
case 'toggle-sa-status':
toggleSAStatus(saId, button.getAttribute('data-current-status'));
break;
case 'delete-sa':
deleteSA(saId);
break;
}
});
});
async function showSADetails(id) {
try {
const response = await fetch(`/api/service-accounts/${id}`);
if (response.ok) {
const sa = await response.json();
document.getElementById('saDetailsContent').innerHTML = createSADetailsContent(sa);
const modal = new bootstrap.Modal(document.getElementById('saDetailsModal'));
modal.show();
} else {
showErrorMessage('Failed to load service account details');
}
} catch (error) {
console.error('Error loading service account details:', error);
showErrorMessage('Failed to load service account details');
}
}
async function toggleSAStatus(id, currentStatus) {
const newStatus = currentStatus === 'Active' ? 'Inactive' : 'Active';
try {
const response = await fetch(`/api/service-accounts/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
});
if (response.ok) {
showSuccessMessage(`Service account ${newStatus === 'Active' ? 'activated' : 'deactivated'}`);
setTimeout(() => window.location.reload(), 1000);
} else {
const error = await response.json();
showErrorMessage('Failed to update status: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error updating status:', error);
showErrorMessage('Failed to update status: ' + error.message);
}
}
async function deleteSA(id) {
if (confirm('Are you sure you want to delete this service account? This action cannot be undone.')) {
try {
const response = await fetch(`/api/service-accounts/${id}`, {
method: 'DELETE'
});
if (response.ok) {
showSuccessMessage('Service account deleted successfully');
setTimeout(() => window.location.reload(), 1000);
} else {
const error = await response.json();
showErrorMessage('Failed to delete: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error deleting service account:', error);
showErrorMessage('Failed to delete: ' + error.message);
}
}
}
async function handleCreateServiceAccount() {
const form = document.getElementById('createSAForm');
const formData = new FormData(form);
const saData = {
parent_user: formData.get('parent_user'),
description: formData.get('description')
};
// Handle expiration if set
const expiration = formData.get('expiration');
if (expiration) {
// Validate the date before using it
const date = new Date(expiration);
const now = new Date();
if (isNaN(date.getTime())) {
showErrorMessage('Invalid expiration date format');
return;
}
// Ensure expiration is in the future
if (date <= now) {
showErrorMessage('Expiration date must be in the future');
return;
}
saData.expiration = date.toISOString();
}
try {
const response = await fetch('/api/service-accounts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(saData)
});
if (response.ok) {
const result = await response.json();
// Hide create modal
const createModal = bootstrap.Modal.getInstance(document.getElementById('createServiceAccountModal'));
createModal.hide();
form.reset();
// Show credentials if returned
if (result.service_account && result.service_account.secret_access_key) {
showCredentials(result.service_account);
} else {
showSuccessMessage('Service account created successfully');
setTimeout(() => window.location.reload(), 1000);
}
} else {
const error = await response.json();
showErrorMessage('Failed to create service account: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error creating service account:', error);
showErrorMessage('Failed to create service account: ' + error.message);
}
}
function showCredentials(serviceAccount) {
// Populate the credentials modal
document.getElementById('displayAccessKey').value = serviceAccount.access_key_id;
document.getElementById('displaySecretKey').value = serviceAccount.secret_access_key;
document.getElementById('exampleAccessKey').textContent = serviceAccount.access_key_id;
document.getElementById('exampleSecretKey').textContent = serviceAccount.secret_access_key;
// Show the modal
const credentialsModal = new bootstrap.Modal(document.getElementById('credentialsModal'));
credentialsModal.show();
}
function closeCredentialsModal() {
const modal = bootstrap.Modal.getInstance(document.getElementById('credentialsModal'));
modal.hide();
// Reload to show the new service account in the list
setTimeout(() => window.location.reload(), 500);
}
function copyCredentialToClipboard(button, elementId) {
const element = document.getElementById(elementId);
const textToCopy = element.value;
// Use modern Clipboard API if available
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(textToCopy).then(() => {
showSuccessMessage('Copied to clipboard!');
}).catch(err => {
console.warn('Clipboard API failed:', err);
// Fallback
fallbackCopyTextToClipboard(element);
});
} else {
// Fallback for older browsers or non-secure contexts
fallbackCopyTextToClipboard(element);
}
// Visual feedback
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i>';
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-success');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 1000);
}
function fallbackCopyTextToClipboard(element) {
element.select();
element.setSelectionRange(0, 99999); // For mobile devices
try {
const successful = document.execCommand('copy');
if (successful) {
showSuccessMessage('Copied to clipboard!');
} else {
showErrorMessage('Failed to copy to clipboard');
}
} catch (err) {
console.error('Fallback copy failed:', err);
showErrorMessage('Failed to copy to clipboard');
}
// Clear selection
window.getSelection().removeAllRanges();
}
function createSADetailsContent(sa) {
// Create DOM elements safely to prevent XSS
const container = document.createElement('div');
container.className = 'row';
// Basic Information column
const col1 = document.createElement('div');
col1.className = 'col-md-6';
const h6_1 = document.createElement('h6');
h6_1.className = 'text-muted';
h6_1.textContent = 'Basic Information';
col1.appendChild(h6_1);
const table1 = document.createElement('table');
table1.className = 'table table-sm';
// ID row
const idRow = document.createElement('tr');
idRow.innerHTML = '<td><strong>ID:</strong></td><td><code></code></td>';
idRow.querySelector('code').textContent = sa.id || '';
table1.appendChild(idRow);
// Parent User row
const parentRow = document.createElement('tr');
parentRow.innerHTML = '<td><strong>Parent User:</strong></td><td></td>';
parentRow.querySelectorAll('td')[1].textContent = sa.parent_user || '';
table1.appendChild(parentRow);
// Access Key row
const keyRow = document.createElement('tr');
keyRow.innerHTML = '<td><strong>Access Key:</strong></td><td><code></code></td>';
keyRow.querySelector('code').textContent = sa.access_key_id || '';
table1.appendChild(keyRow);
// Status row
const statusRow = document.createElement('tr');
const statusTd1 = document.createElement('td');
statusTd1.innerHTML = '<strong>Status:</strong>';
const statusTd2 = document.createElement('td');
const statusBadge = document.createElement('span');
statusBadge.className = sa.status === 'Active' ? 'badge bg-success' : 'badge bg-secondary';
statusBadge.textContent = sa.status || 'Unknown';
statusTd2.appendChild(statusBadge);
statusRow.appendChild(statusTd1);
statusRow.appendChild(statusTd2);
table1.appendChild(statusRow);
col1.appendChild(table1);
container.appendChild(col1);
// Details column
const col2 = document.createElement('div');
col2.className = 'col-md-6';
const h6_2 = document.createElement('h6');
h6_2.className = 'text-muted';
h6_2.textContent = 'Details';
col2.appendChild(h6_2);
const table2 = document.createElement('table');
table2.className = 'table table-sm';
// Description row
const descRow = document.createElement('tr');
descRow.innerHTML = '<td><strong>Description:</strong></td><td></td>';
descRow.querySelectorAll('td')[1].textContent = sa.description || 'Not set';
table2.appendChild(descRow);
// Created row
const createdRow = document.createElement('tr');
createdRow.innerHTML = '<td><strong>Created:</strong></td><td></td>';
try {
createdRow.querySelectorAll('td')[1].textContent = new Date(sa.create_date).toLocaleString();
} catch (e) {
createdRow.querySelectorAll('td')[1].textContent = 'Invalid date';
}
table2.appendChild(createdRow);
// Expiration row
const expRow = document.createElement('tr');
expRow.innerHTML = '<td><strong>Expires:</strong></td><td></td>';
expRow.querySelectorAll('td')[1].textContent = sa.expiration || 'Never';
table2.appendChild(expRow);
col2.appendChild(table2);
container.appendChild(col2);
return container.outerHTML;
}
function showSuccessMessage(message) {
showToast(message, 'success');
}
function showErrorMessage(message) {
showToast(message, 'danger');
}
function showToast(message, type) {
// Create toast container if it doesn't exist
let toastContainer = document.getElementById('toastContainer');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toastContainer';
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
toastContainer.style.zIndex = '9999';
document.body.appendChild(toastContainer);
}
// Create toast element
const toastId = 'toast-' + Date.now();
const toastHTML = `
<div id="${toastId}" class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
${escapeHtml(message)}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement, { autohide: true, delay: 5000 });
toast.show();
// Remove toast element after it's hidden
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
}