diff --git a/static/css/settings/connections.css b/static/css/settings/connections.css new file mode 100644 index 0000000..c206c1d --- /dev/null +++ b/static/css/settings/connections.css @@ -0,0 +1,135 @@ +/* Connection Cards */ +.card { + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.5rem; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +.card-header { + background-color: #fff; + border-bottom: 1px solid rgba(0, 0, 0, 0.125); + padding: 1rem; +} + +.card-body { + padding: 1.25rem; +} + +/* Form Elements */ +.form-label { + font-weight: 500; + margin-bottom: 0.5rem; +} + +.form-control { + border-radius: 0.375rem; + border: 1px solid #ced4da; + padding: 0.5rem 0.75rem; +} + +.form-control:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 0.2rem rgba(var(--primary-color-rgb), 0.25); +} + +.form-text { + font-size: 0.875rem; + color: #6c757d; + margin-top: 0.25rem; +} + +/* Buttons */ +.btn { + border-radius: 0.375rem; + padding: 0.5rem 1rem; + font-weight: 500; +} + +.btn-primary { + background-color: var(--primary-color); + border-color: var(--primary-color); +} + +.btn-primary:hover { + background-color: var(--primary-dark); + border-color: var(--primary-dark); +} + +.btn-outline-primary { + color: var(--primary-color); + border-color: var(--primary-color); +} + +.btn-outline-primary:hover { + background-color: var(--primary-color); + border-color: var(--primary-color); + color: #fff; +} + +/* Modal */ +.modal-content { + border-radius: 0.5rem; + border: none; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); +} + +.modal-header { + border-bottom: 1px solid #dee2e6; + padding: 1rem; +} + +.modal-body { + padding: 1.25rem; +} + +.modal-footer { + border-top: 1px solid #dee2e6; + padding: 1rem; +} + +/* Input Groups */ +.input-group { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: stretch; + width: 100%; +} + +.input-group .form-control { + position: relative; + flex: 1 1 auto; + width: 1%; + min-width: 0; + margin-bottom: 0; +} + +.input-group .btn { + position: relative; + z-index: 2; +} + +/* Icons */ +.fas { + margin-right: 0.5rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .card { + margin-bottom: 1rem; + } + + .btn { + width: 100%; + margin-bottom: 0.5rem; + } + + .input-group { + flex-direction: column; + } + + .input-group .btn { + margin-top: 0.5rem; + } +} \ No newline at end of file diff --git a/static/css/settings/email_templates.css b/static/css/settings/email_templates.css new file mode 100644 index 0000000..3ba5a9a --- /dev/null +++ b/static/css/settings/email_templates.css @@ -0,0 +1,43 @@ +/* Summernote custom styles */ +.note-editor { + margin-bottom: 0; +} +.note-editor.note-frame { + border-color: #dee2e6; + border-radius: 0.375rem; +} +.note-editor.note-frame:focus-within { + border-color: #86b7fe; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.note-toolbar { + background-color: #f8f9fa; + border-top-left-radius: 0.375rem; + border-top-right-radius: 0.375rem; + border-bottom: 1px solid #dee2e6; +} +.note-editing-area { + background-color: #fff; +} +.note-statusbar { + border-top: 1px solid #dee2e6; +} +.note-placeholder { + color: #6c757d; +} + +/* Variable card styles */ +#variableList .card { + border: 1px solid #dee2e6; + transition: all 0.2s ease-in-out; +} +#variableList .card:hover { + border-color: #86b7fe; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.1); +} +#variableList code { + font-size: 0.9em; + padding: 0.2em 0.4em; + background-color: #f8f9fa; + border-radius: 0.25rem; +} \ No newline at end of file diff --git a/static/css/settings/logs.css b/static/css/settings/logs.css new file mode 100644 index 0000000..fe6e841 --- /dev/null +++ b/static/css/settings/logs.css @@ -0,0 +1,95 @@ +/* Log filters form */ +#logFiltersForm .form-label { + font-weight: 500; + color: #495057; +} + +#logFiltersForm .form-control, +#logFiltersForm .form-select { + border-color: #dee2e6; + border-radius: 0.375rem; +} + +#logFiltersForm .form-control:focus, +#logFiltersForm .form-select:focus { + border-color: #86b7fe; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} + +/* Logs table */ +.table { + margin-bottom: 0; +} + +.table th { + font-weight: 600; + color: #495057; + border-bottom-width: 1px; +} + +.table td { + vertical-align: middle; +} + +/* Log level badges */ +.badge { + font-weight: 500; + padding: 0.35em 0.65em; +} + +.badge.bg-info { + background-color: #0dcaf0 !important; +} + +.badge.bg-warning { + background-color: #ffc107 !important; + color: #000; +} + +.badge.bg-danger { + background-color: #dc3545 !important; +} + +.badge.bg-secondary { + background-color: #6c757d !important; +} + +/* Log details modal */ +#logDetailsContent { + max-height: 70vh; + overflow-y: auto; +} + +#logDetailsContent .bg-light { + background-color: #f8f9fa !important; + border: 1px solid #dee2e6; + border-radius: 0.375rem; +} + +/* Pagination */ +.pagination { + margin-bottom: 0; +} + +.page-link { + color: #0d6efd; + border-color: #dee2e6; +} + +.page-link:hover { + color: #0a58ca; + background-color: #e9ecef; + border-color: #dee2e6; +} + +.page-item.active .page-link { + background-color: #0d6efd; + border-color: #0d6efd; +} + +.page-item.disabled .page-link { + color: #6c757d; + pointer-events: none; + background-color: #fff; + border-color: #dee2e6; +} \ No newline at end of file diff --git a/static/css/settings/smtp_settings.css b/static/css/settings/smtp_settings.css new file mode 100644 index 0000000..2884e1c --- /dev/null +++ b/static/css/settings/smtp_settings.css @@ -0,0 +1,105 @@ +/* SMTP Settings Form */ +#smtpSettingsForm .form-label { + font-weight: 500; + color: #495057; +} + +#smtpSettingsForm .form-control, +#smtpSettingsForm .form-select { + border-color: #dee2e6; + border-radius: 0.375rem; +} + +#smtpSettingsForm .form-control:focus, +#smtpSettingsForm .form-select:focus { + border-color: #86b7fe; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} + +/* Info Icons */ +.fa-info-circle { + color: var(--secondary-color); + cursor: pointer; + transition: color 0.2s ease-in-out; +} + +.fa-info-circle:hover { + color: var(--primary-color); +} + +/* Popover Customization */ +.popover { + max-width: 400px; + border: 1px solid #dee2e6; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); +} + +.popover .popover-header { + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; + font-weight: 600; +} + +.popover .popover-body { + padding: 1rem; + color: #495057; +} + +/* Alert Styling */ +.alert { + border: none; + border-radius: 0.375rem; +} + +.alert-info { + background-color: #e7f5ff; + color: #0c5460; +} + +.alert-success { + background-color: #d4edda; + color: #155724; +} + +.alert-danger { + background-color: #f8d7da; + color: #721c24; +} + +/* Code Blocks in Alerts */ +.alert code { + background-color: rgba(0, 0, 0, 0.05); + padding: 0.2em 0.4em; + border-radius: 0.25rem; + font-size: 0.9em; +} + +/* Test Connection Result */ +#testConnectionResult { + margin-top: 1rem; +} + +#testConnectionResult .alert { + margin-bottom: 0; +} + +/* Button Styling */ +.btn-outline-primary { + border-color: #0d6efd; + color: #0d6efd; +} + +.btn-outline-primary:hover { + background-color: #0d6efd; + color: #fff; +} + +.btn-primary { + background-color: #0d6efd; + border-color: #0d6efd; +} + +.btn-primary:hover { + background-color: #0b5ed7; + border-color: #0a58ca; +} \ No newline at end of file diff --git a/static/js/settings/connections.js b/static/js/settings/connections.js new file mode 100644 index 0000000..b639a62 --- /dev/null +++ b/static/js/settings/connections.js @@ -0,0 +1,403 @@ +// Form validation +(function () { + 'use strict' + var forms = document.querySelectorAll('.needs-validation') + Array.prototype.slice.call(forms).forEach(function (form) { + form.addEventListener('submit', function (event) { + if (!form.checkValidity()) { + event.preventDefault() + event.stopPropagation() + } + form.classList.add('was-validated') + }, false) + }) +})() + +// Show success modal +function showSuccess(message) { + const successModal = document.getElementById('successModal'); + if (successModal) { + document.getElementById('successMessage').textContent = message; + new bootstrap.Modal(successModal).show(); + } else { + alert(message); // Fallback if modal doesn't exist + } +} + +// Show error modal +function showError(message) { + const errorModal = document.getElementById('errorModal'); + if (errorModal) { + document.getElementById('errorMessage').textContent = message; + new bootstrap.Modal(errorModal).show(); + } else { + alert(message); // Fallback if modal doesn't exist + } +} + +// Get CSRF token from meta tag +function getCsrfToken() { + const metaTag = document.querySelector('meta[name="csrf-token"]'); + return metaTag ? metaTag.getAttribute('content') : ''; +} + +// Get JWT token using management API key +async function getJwtToken() { + try { + const response = await fetch('/api/admin/management-token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': document.querySelector('meta[name="management-api-key"]').getAttribute('content') + } + }); + + if (!response.ok) { + throw new Error('Failed to get JWT token'); + } + + const data = await response.json(); + return data.token; + } catch (error) { + console.error('Error getting JWT token:', error); + throw error; + } +} + +// Load Gitea Repositories +async function loadGiteaRepos() { + const url = document.getElementById('giteaUrl').value; + const token = document.getElementById('giteaToken').value; + const repoSelect = document.getElementById('giteaRepo'); + const currentRepo = repoSelect.value; // Store current selection + + if (!url || !token) { + showError('Please fill in the server URL and access token'); + return; + } + + try { + const response = await fetch('/api/admin/list-gitea-repos', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': getCsrfToken() + }, + body: JSON.stringify({ url, token }) + }); + + const data = await response.json(); + + if (response.ok) { + repoSelect.innerHTML = ''; + + data.repositories.forEach(repo => { + const option = document.createElement('option'); + option.value = repo.full_name; + option.textContent = repo.full_name; + if (repo.full_name === currentRepo) { // Restore selection + option.selected = true; + } + repoSelect.appendChild(option); + }); + } else { + throw new Error(data.message || 'Failed to load repositories'); + } + } catch (error) { + showError(error.message); + } +} + +// Test Git Connection +async function testGitConnection(provider) { + const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal')); + const messageElement = document.getElementById('saveConnectionMessage'); + messageElement.textContent = 'Testing connection...'; + messageElement.className = ''; + saveModal.show(); + + try { + const url = document.getElementById('giteaUrl').value; + const username = document.getElementById('giteaUsername').value; + const token = document.getElementById('giteaToken').value; + + if (!url || !username || !token) { + throw new Error('Please fill in all required fields'); + } + + const data = { + provider: 'gitea', + url: url, + username: username, + token: token + }; + + const response = await fetch('/settings/test-git-connection', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': getCsrfToken() + }, + body: JSON.stringify(data) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Connection test failed'); + } + + messageElement.textContent = 'Connection test successful!'; + messageElement.className = 'text-success'; + } catch (error) { + messageElement.textContent = error.message || 'Connection test failed'; + messageElement.className = 'text-danger'; + } +} + +// Test Portainer Connection +async function testPortainerConnection() { + const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal')); + const messageElement = document.getElementById('saveConnectionMessage'); + messageElement.textContent = 'Testing connection...'; + messageElement.className = ''; + saveModal.show(); + + try { + const url = document.getElementById('portainerUrl').value; + const apiKey = document.getElementById('portainerApiKey').value; + + if (!url || !apiKey) { + throw new Error('Please fill in all required fields'); + } + + const response = await fetch('/api/admin/test-portainer-connection', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': getCsrfToken() + }, + body: JSON.stringify({ + url: url, + api_key: apiKey + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Connection test failed'); + } + + messageElement.textContent = 'Connection test successful!'; + messageElement.className = 'text-success'; + } catch (error) { + messageElement.textContent = error.message || 'Connection test failed'; + messageElement.className = 'text-danger'; + } +} + +// Test NGINX Connection +async function testNginxConnection() { + const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal')); + const messageElement = document.getElementById('saveConnectionMessage'); + messageElement.textContent = 'Testing connection...'; + messageElement.className = ''; + saveModal.show(); + + try { + const url = document.getElementById('nginxUrl').value; + const username = document.getElementById('nginxUsername').value; + const password = document.getElementById('nginxPassword').value; + + if (!url || !username || !password) { + throw new Error('Please fill in all required fields'); + } + + // First, get the token + const tokenResponse = await fetch(`${url}/api/tokens`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + identity: username, + secret: password + }) + }); + + if (!tokenResponse.ok) { + throw new Error('Failed to authenticate with NGINX Proxy Manager'); + } + + const tokenData = await tokenResponse.json(); + const token = tokenData.token; + + // Now test the connection using the token + const response = await fetch(`${url}/api/nginx/proxy-hosts`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Failed to connect to NGINX Proxy Manager'); + } + + messageElement.textContent = 'Connection test successful!'; + messageElement.className = 'text-success'; + } catch (error) { + messageElement.textContent = error.message || 'Connection test failed'; + messageElement.className = 'text-danger'; + } +} + +// Save Portainer Connection +async function savePortainerConnection(event) { + event.preventDefault(); + const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal')); + const messageElement = document.getElementById('saveConnectionMessage'); + messageElement.textContent = ''; + messageElement.className = ''; + + try { + const url = document.getElementById('portainerUrl').value; + const apiKey = document.getElementById('portainerApiKey').value; + + if (!url || !apiKey) { + throw new Error('Please fill in all required fields'); + } + + const response = await fetch('/settings/save-portainer-connection', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': getCsrfToken() + }, + body: JSON.stringify({ + url: url, + api_key: apiKey + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to save settings'); + } + + messageElement.textContent = 'Settings saved successfully!'; + messageElement.className = 'text-success'; + } catch (error) { + messageElement.textContent = error.message || 'Failed to save settings'; + messageElement.className = 'text-danger'; + } + + saveModal.show(); +} + +// Save NGINX Connection +async function saveNginxConnection(event) { + event.preventDefault(); + const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal')); + const messageElement = document.getElementById('saveConnectionMessage'); + messageElement.textContent = ''; + messageElement.className = ''; + + try { + const url = document.getElementById('nginxUrl').value; + const username = document.getElementById('nginxUsername').value; + const password = document.getElementById('nginxPassword').value; + + if (!url || !username || !password) { + throw new Error('Please fill in all required fields'); + } + + const response = await fetch('/settings/save-nginx-connection', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': getCsrfToken() + }, + body: JSON.stringify({ + url: url, + username: username, + password: password + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to save settings'); + } + + messageElement.textContent = 'Settings saved successfully!'; + messageElement.className = 'text-success'; + } catch (error) { + messageElement.textContent = error.message || 'Failed to save settings'; + messageElement.className = 'text-danger'; + } + + saveModal.show(); +} + +// Save Git Connection +async function saveGitConnection(event, provider) { + event.preventDefault(); + const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal')); + const messageElement = document.getElementById('saveConnectionMessage'); + messageElement.textContent = ''; + messageElement.className = ''; + + try { + const url = document.getElementById('giteaUrl').value; + const username = document.getElementById('giteaUsername').value; + const token = document.getElementById('giteaToken').value; + const repo = document.getElementById('giteaRepo').value; + const password = document.getElementById('giteaPassword').value; + const otp = document.getElementById('giteaOtp').value; + + if (!url || !username || !token || !repo) { + throw new Error('Please fill in all required fields'); + } + + const data = { + provider: 'gitea', + url: url, + username: username, + token: token, + repo: repo, + password: password, + otp: otp + }; + + const response = await fetch('/settings/save-git-connection', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': getCsrfToken() + }, + body: JSON.stringify(data) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to save settings'); + } + + messageElement.textContent = 'Settings saved successfully!'; + messageElement.className = 'text-success'; + } catch (error) { + messageElement.textContent = error.message || 'Failed to save settings'; + messageElement.className = 'text-danger'; + } + + saveModal.show(); +} + +// Initialize on page load +document.addEventListener('DOMContentLoaded', function() { + const gitSettings = JSON.parse(document.querySelector('meta[name="git-settings"]').getAttribute('content')); + if (gitSettings && gitSettings.provider === 'gitea' && gitSettings.url && gitSettings.token) { + loadGiteaRepos(); + } +}); \ No newline at end of file diff --git a/static/js/settings/email_templates.js b/static/js/settings/email_templates.js new file mode 100644 index 0000000..7cd4dce --- /dev/null +++ b/static/js/settings/email_templates.js @@ -0,0 +1,258 @@ +// Template variables mapping +const templateVariables = { + 'Account Created': { + 'user.username': 'The username of the created account', + 'user.email': 'The email address of the created account', + 'user.company': 'The company name of the user', + 'user.position': 'The position of the user in their company', + 'created_at': 'The date and time when the account was created', + 'site.company_name': 'The name of your company', + 'site.company_website': 'Your company website URL', + 'setup_link': 'The link to set up the user\'s password (expires in 24 hours)', + 'created_by': 'The name of the admin who created the account' + }, + 'Password Reset': { + 'user.username': 'The username of the account', + 'user.email': 'The email address of the account', + 'reset_link': 'The password reset link', + 'expiry_time': 'The time when the reset link expires', + 'site.company_name': 'The name of your company', + 'site.company_website': 'Your company website URL' + }, + 'Account Deleted': { + 'user.username': 'The username of the deleted account', + 'user.email': 'The email address of the deleted account', + 'deleted_at': 'The date and time of deletion', + 'deleted_by': 'The username of the admin who deleted the account', + 'site.company_name': 'The name of your company', + 'site.company_website': 'Your company website URL' + }, + 'Account Updated': { + 'user.username': 'The username of the updated account', + 'user.email': 'The email address of the account', + 'updated_fields': 'The fields that were updated', + 'updated_at': 'The date and time of the update', + 'updated_by': 'The username of who made the update', + 'site.company_name': 'The name of your company', + 'site.company_website': 'Your company website URL' + }, + 'Room Invite': { + 'user.username': 'The username of the invited user', + 'user.email': 'The email address of the invited user', + 'room.name': 'The name of the room', + 'room.description': 'The description of the room', + 'inviter.username': 'The username of the user who sent the invite', + 'inviter.email': 'The email address of the inviter', + 'invite_link': 'The link to accept the room invite', + 'site.company_name': 'The name of your company', + 'site.company_website': 'Your company website URL' + }, + 'Room Invite Removed': { + 'user.username': 'The username of the user', + 'user.email': 'The email address of the user', + 'room.name': 'The name of the room', + 'room.description': 'The description of the room', + 'remover.username': 'The username of the user who removed the invite', + 'remover.email': 'The email address of the remover', + 'removed_at': 'The date and time when the invite was removed', + 'site.company_name': 'The name of your company', + 'site.company_website': 'Your company website URL' + }, + 'Conversation Invite': { + 'user.username': 'The username of the invited user', + 'user.email': 'The email address of the invited user', + 'conversation.name': 'The name of the conversation', + 'conversation.description': 'The description of the conversation', + 'inviter.username': 'The username of the user who sent the invite', + 'inviter.email': 'The email address of the inviter', + 'invite_link': 'The link to accept the conversation invite', + 'site.company_name': 'The name of your company', + 'site.company_website': 'Your company website URL' + }, + 'Conversation Invite Removed': { + 'user.username': 'The username of the user', + 'user.email': 'The email address of the user', + 'conversation.name': 'The name of the conversation', + 'conversation.description': 'The description of the conversation', + 'remover.username': 'The username of the user who removed the invite', + 'remover.email': 'The email address of the remover', + 'removed_at': 'The date and time when the invite was removed', + 'site.company_name': 'The name of your company', + 'site.company_website': 'Your company website URL' + }, + 'Conversation Message': { + 'user.username': 'The username of the recipient', + 'user.email': 'The email address of the recipient', + 'sender.username': 'The username of the message sender', + 'sender.email': 'The email address of the sender', + 'message.content': 'The content of the message', + 'message.created_at': 'The date and time when the message was sent', + 'conversation.name': 'The name of the conversation', + 'conversation.description': 'The description of the conversation', + 'message_link': 'The link to view the message', + 'site.company_name': 'The name of your company', + 'site.company_website': 'Your company website URL' + } +}; + +// Function to update available variables display +function updateAvailableVariables(templateType) { + const variables = templateVariables[templateType] || {}; + const variableList = document.getElementById('variableList'); + const availableVariables = document.getElementById('availableVariables'); + + if (Object.keys(variables).length === 0) { + availableVariables.style.display = 'none'; + return; + } + + let html = '
'; + for (const [variable, description] of Object.entries(variables)) { + html += ` +
+
+
+ {{ '{{ ' }}${variable}{{ ' }}' }} +

${description}

+
+
+
+ `; + } + html += '
'; + + variableList.innerHTML = html; + availableVariables.style.display = 'block'; +} + +// Initialize on page load +document.addEventListener('DOMContentLoaded', function() { + // Initialize Summernote + $('#templateBody').summernote({ + height: 300, + toolbar: [ + ['style', ['style']], + ['font', ['bold', 'underline', 'italic', 'clear']], + ['color', ['color']], + ['para', ['ul', 'ol', 'paragraph']], + ['table', ['table']], + ['insert', ['link']], + ['view', ['fullscreen', 'codeview', 'help']] + ], + styleTags: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + placeholder: 'Enter your email template content here...', + callbacks: { + onImageUpload: function(files) { + // Disable image upload for email templates + return false; + } + }, + popover: { + image: [], + link: [], + air: [] + } + }); + + // Handle template selection + $('#templateSelect').on('change', function() { + const selectedOption = this.options[this.selectedIndex]; + const subject = selectedOption.dataset.subject || ''; + const body = selectedOption.dataset.body || ''; + const templateType = selectedOption.dataset.type || ''; + + // Show the template editor + $('#templateEditor').show(); + + // Update the form fields + $('#templateSubject').val(subject); + $('#templateBody').summernote('code', body); + updateAvailableVariables(templateType); + }); + + // Check for initially selected template + const templateSelect = document.getElementById('templateSelect'); + if (templateSelect.value) { + const selectedOption = templateSelect.options[templateSelect.selectedIndex]; + const templateType = selectedOption.dataset.type || ''; + updateAvailableVariables(templateType); + // Show the template editor if a template is initially selected + $('#templateEditor').show(); + } + + // Handle template save + $('#templateForm').on('submit', function(event) { + event.preventDefault(); + const templateId = $('#templateSelect').val(); + const subject = $('#templateSubject').val(); + const body = $('#templateBody').summernote('code'); + + if (!templateId) { + alert('Please select a template first'); + return; + } + + // Show loading state + const saveButton = this.querySelector('button[type="submit"]'); + const originalText = saveButton.innerHTML; + saveButton.disabled = true; + saveButton.innerHTML = 'Saving...'; + + // Get CSRF token from meta tag + const csrfToken = document.querySelector('meta[name="csrf-token"]').content; + + // Send AJAX request + fetch(`/settings/email-templates/${templateId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify({ + subject: subject, + body: body + }) + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.error) { + throw new Error(data.error); + } + + // Show success message + const alert = document.createElement('div'); + alert.className = 'alert alert-success alert-dismissible fade show mt-3'; + alert.innerHTML = ` + Template saved successfully + + `; + document.querySelector('.card-body').appendChild(alert); + + // Update the select option's data attributes + const option = document.getElementById('templateSelect').options[document.getElementById('templateSelect').selectedIndex]; + option.dataset.subject = subject; + option.dataset.body = body; + }) + .catch(error => { + // Show error message + const alert = document.createElement('div'); + alert.className = 'alert alert-danger alert-dismissible fade show mt-3'; + alert.innerHTML = ` + ${error.message || 'Failed to save template'} + + `; + document.querySelector('.card-body').appendChild(alert); + }) + .finally(() => { + // Restore button state + saveButton.disabled = false; + saveButton.innerHTML = originalText; + }); + }); +}); \ No newline at end of file diff --git a/static/js/settings/logs.js b/static/js/settings/logs.js new file mode 100644 index 0000000..ec115cd --- /dev/null +++ b/static/js/settings/logs.js @@ -0,0 +1,181 @@ +// Function to view log details +function viewLogDetails(logId) { + fetch(`/api/logs/${logId}`) + .then(response => { + if (!response.ok) { + throw new Error('Failed to fetch log details'); + } + return response.json(); + }) + .then(log => { + const content = document.getElementById('logDetailsContent'); + + // Format the details in a readable way + let details = log.details; + let formattedDetails = 'No details available'; + + if (details) { + if (typeof details === 'string') { + formattedDetails = details.replace(/\n/g, '
'); + } else if (typeof details === 'object') { + formattedDetails = JSON.stringify(details, null, 2).replace(/\n/g, '
'); + } else { + formattedDetails = String(details); + } + } + + content.innerHTML = ` +
+ Timestamp: ${new Date(log.timestamp).toLocaleString()} +
+
+ Level: ${log.level} +
+
+ Category: ${log.category || '-'} +
+
+ Action: ${log.action || '-'} +
+
+ Description: ${log.description || '-'} +
+
+ User: ${log.user || '-'} +
+
+ IP Address: ${log.ip_address || '-'} +
+
+ Details: +
+ ${formattedDetails} +
+
+ `; + + // Show modal + const modal = new bootstrap.Modal(document.getElementById('logDetailsModal')); + modal.show(); + }) + .catch(error => { + console.error('Error viewing log details:', error); + showToast('Error loading log details', 'error'); + }); +} + +// Helper function to get log level badge class +function getLogLevelBadgeClass(level) { + const classes = { + 'INFO': 'info', + 'WARNING': 'warning', + 'ERROR': 'danger', + 'DEBUG': 'secondary' + }; + return classes[level] || 'secondary'; +} + +// Function to fetch logs +async function fetchLogs(page = 1) { + try { + const formData = new FormData(document.getElementById('logFiltersForm')); + const params = new URLSearchParams(); + + // Add form data to params + for (let [key, value] of formData.entries()) { + if (value) params.append(key, value); + } + + // Add page number + params.append('page', page); + + const response = await fetch(`/api/logs?${params.toString()}`); + if (!response.ok) throw new Error('Failed to fetch logs'); + + const data = await response.json(); + const logsTableBody = document.getElementById('logsTableBody'); + logsTableBody.innerHTML = ''; + + data.logs.forEach(log => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${new Date(log.timestamp).toLocaleString()} + ${log.level} + ${log.category || '-'} + ${log.action || '-'} + ${log.description || '-'} + ${log.user || '-'} + ${log.ip_address || '-'} + + + + `; + logsTableBody.appendChild(row); + }); + + // Update pagination + updateLogsPagination(data.current_page, data.total_pages); + } catch (error) { + console.error('Error fetching logs:', error); + showToast('Error loading logs', 'error'); + } +} + +// Function to update logs pagination +function updateLogsPagination(currentPage, totalPages) { + const pagination = document.getElementById('logsPagination'); + if (!pagination) return; + + pagination.innerHTML = ''; + + // Previous button + const prevLi = document.createElement('li'); + prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`; + prevLi.innerHTML = ` + + `; + pagination.appendChild(prevLi); + + // Page numbers + for (let i = 1; i <= totalPages; i++) { + const li = document.createElement('li'); + li.className = `page-item ${i === currentPage ? 'active' : ''}`; + li.innerHTML = ` + + `; + pagination.appendChild(li); + } + + // Next button + const nextLi = document.createElement('li'); + nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`; + nextLi.innerHTML = ` + + `; + pagination.appendChild(nextLi); +} + +// Initialize on page load +document.addEventListener('DOMContentLoaded', function() { + // Initial load + fetchLogs(); + + // Form submit handler + document.getElementById('logFiltersForm').addEventListener('submit', function(e) { + e.preventDefault(); + fetchLogs(1); + }); + + // Form reset handler + document.getElementById('logFiltersForm').addEventListener('reset', function(e) { + e.preventDefault(); + this.reset(); + fetchLogs(1); + }); +}); \ No newline at end of file diff --git a/static/js/settings/smtp_settings.js b/static/js/settings/smtp_settings.js new file mode 100644 index 0000000..9da8fe9 --- /dev/null +++ b/static/js/settings/smtp_settings.js @@ -0,0 +1,51 @@ +// Initialize all popovers +document.addEventListener('DOMContentLoaded', function() { + var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')); + var popoverList = popoverTriggerList.map(function(popoverTriggerEl) { + return new bootstrap.Popover(popoverTriggerEl, { + html: true, + trigger: 'hover', + placement: 'right' + }); + }); +}); + +function testSmtpConnection() { + const form = document.getElementById('smtpSettingsForm'); + const resultDiv = document.getElementById('testConnectionResult'); + const button = event.target; + const originalText = button.innerHTML; + + // Disable button and show loading state + button.disabled = true; + button.innerHTML = 'Testing...'; + + // Get form data + const formData = new FormData(form); + + // Send test request + fetch('/settings/test-smtp', { + method: 'POST', + headers: { + 'X-CSRF-Token': document.querySelector('input[name="csrf_token"]').value, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(Object.fromEntries(formData)) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + resultDiv.innerHTML = '
Connection successful!
'; + } else { + resultDiv.innerHTML = `
Connection failed: ${data.error}
`; + } + }) + .catch(error => { + resultDiv.innerHTML = `
Error: ${error.message}
`; + }) + .finally(() => { + // Restore button state + button.disabled = false; + button.innerHTML = originalText; + }); +} \ No newline at end of file diff --git a/templates/settings/tabs/connections.html b/templates/settings/tabs/connections.html index 2a27be6..87a2a7c 100644 --- a/templates/settings/tabs/connections.html +++ b/templates/settings/tabs/connections.html @@ -1,6 +1,10 @@ {% from "settings/components/connection_modals.html" import connection_modals %} {% macro connections_tab(portainer_settings, nginx_settings, site_settings, git_settings) %} + + + +
@@ -180,409 +184,6 @@ {{ connection_modals() }} - + + {% endmacro %} \ No newline at end of file diff --git a/templates/settings/tabs/email_templates.html b/templates/settings/tabs/email_templates.html index 3198615..2cb3af9 100644 --- a/templates/settings/tabs/email_templates.html +++ b/templates/settings/tabs/email_templates.html @@ -68,266 +68,8 @@ - + +