mirror of
https://github.com/stellarshenson/stellars-jupyterhub-ds.git
synced 2026-03-08 06:00:29 +00:00
- Remove unused *.html from services/jupyterhub/templates/ - Keep only certs/ subdirectory in templates/ - Rename templates_enhanced to html_templates_enhanced - Update Dockerfile COPY paths
316 lines
12 KiB
HTML
316 lines
12 KiB
HTML
{% extends "page.html" %}
|
|
{% block main %}
|
|
<div id="react-admin-hook">
|
|
<script id="jupyterhub-admin-config">
|
|
window.api_page_limit = parseInt("{{ api_page_limit|safe }}")
|
|
window.base_url = "{{ base_url|safe }}"
|
|
</script>
|
|
<script src={{ static_url("js/admin-react.js") }}></script>
|
|
</div>
|
|
|
|
<!-- User Credentials Modal -->
|
|
<div class="modal fade" id="user-credentials-modal" tabindex="-1" role="dialog" aria-labelledby="userCredentialsModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="userCredentialsModalLabel">
|
|
<i class="fa fa-user-plus me-2"></i>New Users Created
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-success">
|
|
<strong>Success:</strong> Users created and authorized.
|
|
Share the credentials below with each user.
|
|
</div>
|
|
<div class="mb-3">
|
|
<button type="button" class="btn btn-outline-secondary me-2" id="copy-credentials-btn">
|
|
<i class="fa fa-copy me-1"></i>Copy to Clipboard
|
|
</button>
|
|
<button type="button" class="btn btn-primary" id="download-credentials-btn">
|
|
<i class="fa fa-download me-1"></i>Download as TXT
|
|
</button>
|
|
</div>
|
|
<div style="max-height: 300px; overflow-y: auto;">
|
|
<table class="table table-striped" id="credentials-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Username</th>
|
|
<th>Password</th>
|
|
<th style="width: 40px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="credentials-body">
|
|
<!-- Populated dynamically -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading Spinner Modal -->
|
|
<div class="modal fade" id="loading-modal" tabindex="-1" role="dialog" data-bs-backdrop="static" data-bs-keyboard="false" aria-hidden="true">
|
|
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-body text-center py-4">
|
|
<div class="spinner-border text-primary mb-3" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<div>Generating credentials...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock main %}
|
|
|
|
{% block footer %}
|
|
<div class="py-2 px-4 bg-body-tertiary small version_footer">JupyterHub {{ server_version }}</div>
|
|
{% endblock footer %}
|
|
|
|
{% block script %}
|
|
{{ super() }}
|
|
<script type="text/javascript">
|
|
(function() {
|
|
const baseUrl = "{{ base_url|safe }}";
|
|
|
|
// Store for tracking newly created users
|
|
let pendingUsernames = [];
|
|
|
|
// Helper to get CSRF token
|
|
function getCookie(name) {
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
|
}
|
|
|
|
// Loading spinner functions
|
|
let loadingModal = null;
|
|
function showLoadingSpinner() {
|
|
const modalEl = document.getElementById('loading-modal');
|
|
if (!loadingModal) {
|
|
loadingModal = new bootstrap.Modal(modalEl);
|
|
}
|
|
loadingModal.show();
|
|
}
|
|
function hideLoadingSpinner() {
|
|
if (loadingModal) {
|
|
loadingModal.hide();
|
|
}
|
|
}
|
|
|
|
// Intercept fetch to detect user creation
|
|
const originalFetch = window.fetch;
|
|
window.fetch = async function(...args) {
|
|
const [urlArg, options] = args;
|
|
// Convert URL object to string if needed
|
|
const url = typeof urlArg === 'string' ? urlArg : urlArg.toString();
|
|
const method = options?.method || 'GET';
|
|
|
|
// console.log('[Admin Fetch]', method, url);
|
|
|
|
// Check for user creation - POST to api/users (may have ?_xsrf query param)
|
|
// Exclude URLs with other query params like include_stopped_servers
|
|
const urlPath = url.split('?')[0];
|
|
const isUserCreation = method === 'POST' && urlPath.endsWith('api/users');
|
|
|
|
// Capture request body before fetch (for batch user creation)
|
|
let requestUsernames = [];
|
|
if (isUserCreation) {
|
|
// console.log('[Admin] User creation POST detected!');
|
|
try {
|
|
if (options.body) {
|
|
const bodyData = JSON.parse(options.body);
|
|
if (bodyData.usernames && Array.isArray(bodyData.usernames)) {
|
|
requestUsernames = bodyData.usernames;
|
|
// console.log('[Admin] Batch user creation request:', requestUsernames);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Body might not be JSON, ignore
|
|
}
|
|
}
|
|
|
|
const response = await originalFetch.apply(this, args);
|
|
// console.log('[Admin Fetch] Response status:', response.status);
|
|
|
|
// Check if this is a POST to create users
|
|
if (isUserCreation) {
|
|
try {
|
|
// Only process successful responses
|
|
if (response.ok || response.status === 201) {
|
|
// Clone response to read body
|
|
const clonedResponse = response.clone();
|
|
const responseData = await clonedResponse.json();
|
|
|
|
// Extract created usernames from response
|
|
let createdUsers = [];
|
|
if (Array.isArray(responseData)) {
|
|
// Batch response: array of user objects
|
|
createdUsers = responseData.map(u => u.name);
|
|
} else if (responseData.name) {
|
|
// Single user response
|
|
createdUsers = [responseData.name];
|
|
}
|
|
|
|
if (createdUsers.length > 0) {
|
|
// console.log('[Admin] Users created:', createdUsers);
|
|
pendingUsernames.push(...createdUsers);
|
|
|
|
// Show loading spinner
|
|
showLoadingSpinner();
|
|
|
|
// Debounce - wait for batch completion then fetch credentials
|
|
clearTimeout(window._credentialsFetchTimeout);
|
|
window._credentialsFetchTimeout = setTimeout(() => {
|
|
if (pendingUsernames.length > 0) {
|
|
fetchAndShowCredentials([...pendingUsernames]);
|
|
pendingUsernames = [];
|
|
}
|
|
}, 1000);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// console.error('[Admin] Error processing user creation:', e);
|
|
}
|
|
}
|
|
|
|
return response;
|
|
};
|
|
|
|
// Fetch credentials for created users and show modal
|
|
async function fetchAndShowCredentials(usernames) {
|
|
// console.log('[Admin] Fetching credentials for:', usernames);
|
|
|
|
try {
|
|
const response = await originalFetch(`${baseUrl}api/admin/credentials`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-XSRFToken': getCookie('_xsrf')
|
|
},
|
|
body: JSON.stringify({ usernames: usernames })
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
// console.log('[Admin] Credentials received:', data);
|
|
|
|
if (data.credentials && data.credentials.length > 0) {
|
|
showCredentialsModal(data.credentials);
|
|
}
|
|
} else {
|
|
// console.error('[Admin] Failed to fetch credentials:', response.status);
|
|
hideLoadingSpinner();
|
|
}
|
|
} catch (e) {
|
|
// console.error('[Admin] Error fetching credentials:', e);
|
|
hideLoadingSpinner();
|
|
}
|
|
}
|
|
|
|
// Show modal with credentials
|
|
function showCredentialsModal(credentials) {
|
|
hideLoadingSpinner();
|
|
const tbody = document.getElementById('credentials-body');
|
|
tbody.innerHTML = '';
|
|
|
|
credentials.forEach(cred => {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>${escapeHtml(cred.username)}</td>
|
|
<td>${escapeHtml(cred.password)}</td>
|
|
<td><i class="fa fa-copy copy-row-btn" style="opacity: 0.4; cursor: pointer;" title="Copy to clipboard"></i></td>
|
|
`;
|
|
// Add click handler for row copy
|
|
row.querySelector('.copy-row-btn').addEventListener('click', function() {
|
|
const text = 'Username: ' + cred.username + '\nPassword: ' + cred.password;
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
this.classList.remove('fa-copy');
|
|
this.classList.add('fa-check');
|
|
this.style.opacity = '1';
|
|
setTimeout(() => {
|
|
this.classList.remove('fa-check');
|
|
this.classList.add('fa-copy');
|
|
this.style.opacity = '0.4';
|
|
}, 1500);
|
|
});
|
|
});
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// Store credentials for download/copy
|
|
window._currentCredentials = credentials;
|
|
|
|
// Show modal
|
|
const modalEl = document.getElementById('user-credentials-modal');
|
|
const modal = new bootstrap.Modal(modalEl);
|
|
modal.show();
|
|
}
|
|
|
|
// HTML escape helper
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Format credentials for text output
|
|
function formatCredentialsText(credentials) {
|
|
let text = 'JupyterHub User Credentials\n';
|
|
text += '===========================\n\n';
|
|
credentials.forEach(cred => {
|
|
text += `Username: ${cred.username}\n`;
|
|
text += `Password: ${cred.password}\n\n`;
|
|
});
|
|
text += `Generated: ${new Date().toISOString()}\n`;
|
|
return text;
|
|
}
|
|
|
|
// Copy to clipboard handler
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.getElementById('copy-credentials-btn')?.addEventListener('click', function() {
|
|
if (window._currentCredentials) {
|
|
const text = formatCredentialsText(window._currentCredentials);
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
const btn = this;
|
|
const originalText = btn.innerHTML;
|
|
btn.innerHTML = '<i class="fa fa-check me-1"></i>Copied!';
|
|
btn.classList.remove('btn-outline-secondary');
|
|
btn.classList.add('btn-success');
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalText;
|
|
btn.classList.remove('btn-success');
|
|
btn.classList.add('btn-outline-secondary');
|
|
}, 2000);
|
|
}).catch(err => {
|
|
// console.error('[Admin] Failed to copy:', err);
|
|
alert('Failed to copy to clipboard');
|
|
});
|
|
}
|
|
});
|
|
|
|
// Download as TXT handler
|
|
document.getElementById('download-credentials-btn')?.addEventListener('click', function() {
|
|
if (window._currentCredentials) {
|
|
const text = formatCredentialsText(window._currentCredentials);
|
|
const blob = new Blob([text], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `jupyterhub-credentials-${new Date().toISOString().slice(0,10)}.txt`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock script %}
|