Splitting css and JS files on settings pages
This commit is contained in:
135
static/css/settings/connections.css
Normal file
135
static/css/settings/connections.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
static/css/settings/email_templates.css
Normal file
43
static/css/settings/email_templates.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
95
static/css/settings/logs.css
Normal file
95
static/css/settings/logs.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
105
static/css/settings/smtp_settings.css
Normal file
105
static/css/settings/smtp_settings.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
403
static/js/settings/connections.js
Normal file
403
static/js/settings/connections.js
Normal file
@@ -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 = '<option value="">Select a repository</option>';
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
258
static/js/settings/email_templates.js
Normal file
258
static/js/settings/email_templates.js
Normal file
@@ -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 = '<div class="row">';
|
||||||
|
for (const [variable, description] of Object.entries(variables)) {
|
||||||
|
html += `
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<code class="text-primary">{{ '{{ ' }}${variable}{{ ' }}' }}</code>
|
||||||
|
<p class="text-muted small mb-0">${description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
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 = '<i class="fas fa-spinner fa-spin me-2"></i>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 = `
|
||||||
|
<i class="fas fa-check-circle me-2"></i>Template saved successfully
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<i class="fas fa-exclamation-circle me-2"></i>${error.message || 'Failed to save template'}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
`;
|
||||||
|
document.querySelector('.card-body').appendChild(alert);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// Restore button state
|
||||||
|
saveButton.disabled = false;
|
||||||
|
saveButton.innerHTML = originalText;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
181
static/js/settings/logs.js
Normal file
181
static/js/settings/logs.js
Normal file
@@ -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, '<br>');
|
||||||
|
} else if (typeof details === 'object') {
|
||||||
|
formattedDetails = JSON.stringify(details, null, 2).replace(/\n/g, '<br>');
|
||||||
|
} else {
|
||||||
|
formattedDetails = String(details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Timestamp:</strong> ${new Date(log.timestamp).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Level:</strong> <span class="badge bg-${getLogLevelBadgeClass(log.level)}">${log.level}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Category:</strong> ${log.category || '-'}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Action:</strong> ${log.action || '-'}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Description:</strong> ${log.description || '-'}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>User:</strong> ${log.user || '-'}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>IP Address:</strong> ${log.ip_address || '-'}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Details:</strong>
|
||||||
|
<div class="mt-2 p-3 bg-light rounded">
|
||||||
|
${formattedDetails}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<td>${new Date(log.timestamp).toLocaleString()}</td>
|
||||||
|
<td><span class="badge bg-${getLogLevelBadgeClass(log.level)}">${log.level}</span></td>
|
||||||
|
<td>${log.category || '-'}</td>
|
||||||
|
<td>${log.action || '-'}</td>
|
||||||
|
<td>${log.description || '-'}</td>
|
||||||
|
<td>${log.user || '-'}</td>
|
||||||
|
<td>${log.ip_address || '-'}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="viewLogDetails(${log.id})">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<button class="page-link" onclick="fetchLogs(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>
|
||||||
|
<i class="fas fa-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<button class="page-link" onclick="fetchLogs(${i})">${i}</button>
|
||||||
|
`;
|
||||||
|
pagination.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next button
|
||||||
|
const nextLi = document.createElement('li');
|
||||||
|
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
|
||||||
|
nextLi.innerHTML = `
|
||||||
|
<button class="page-link" onclick="fetchLogs(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>
|
||||||
|
<i class="fas fa-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
51
static/js/settings/smtp_settings.js
Normal file
51
static/js/settings/smtp_settings.js
Normal file
@@ -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 = '<i class="fas fa-spinner fa-spin me-2"></i>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 = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>Connection successful!</div>';
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="fas fa-times-circle me-2"></i>Connection failed: ${data.error}</div>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="fas fa-times-circle me-2"></i>Error: ${error.message}</div>`;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// Restore button state
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
{% from "settings/components/connection_modals.html" import connection_modals %}
|
{% from "settings/components/connection_modals.html" import connection_modals %}
|
||||||
|
|
||||||
{% macro connections_tab(portainer_settings, nginx_settings, site_settings, git_settings) %}
|
{% macro connections_tab(portainer_settings, nginx_settings, site_settings, git_settings) %}
|
||||||
|
<!-- Meta tags for JavaScript -->
|
||||||
|
<meta name="management-api-key" content="{{ site_settings.management_api_key }}">
|
||||||
|
<meta name="git-settings" content="{{ git_settings|tojson|safe }}">
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Portainer Connection Card -->
|
<!-- Portainer Connection Card -->
|
||||||
@@ -180,409 +184,6 @@
|
|||||||
|
|
||||||
{{ connection_modals() }}
|
{{ connection_modals() }}
|
||||||
|
|
||||||
<script>
|
<!-- Load JavaScript -->
|
||||||
// Form validation
|
<script src="{{ url_for('static', filename='js/settings/connections.js') }}?v={{ 'js/settings/connections.js'|asset_version }}"></script>
|
||||||
(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': '{{ site_settings.management_api_key }}'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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 = '<option value="">Select a repository</option>';
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load repositories on page load if settings exist
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const gitSettings = JSON.parse('{{ git_settings|tojson|safe }}');
|
|
||||||
if (gitSettings && gitSettings.provider === 'gitea' && gitSettings.url && gitSettings.token) {
|
|
||||||
loadGiteaRepos();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
@@ -68,266 +68,8 @@
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.css" rel="stylesheet">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<!-- Load JavaScript -->
|
||||||
// Template variables mapping
|
<script src="{{ url_for('static', filename='js/settings/email_templates.js') }}?v={{ 'js/settings/email_templates.js'|asset_version }}"></script>
|
||||||
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 = '<div class="row">';
|
|
||||||
for (const [variable, description] of Object.entries(variables)) {
|
|
||||||
html += `
|
|
||||||
<div class="col-md-6 mb-2">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body p-2">
|
|
||||||
<code class="text-primary">{{ '{{ ' }}${variable}{{ ' }}' }}</code>
|
|
||||||
<p class="text-muted small mb-0">${description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
variableList.innerHTML = html;
|
|
||||||
availableVariables.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for document and jQuery to be ready
|
|
||||||
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 = '<i class="fas fa-spinner fa-spin me-2"></i>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 = `
|
|
||||||
<i class="fas fa-check-circle me-2"></i>Template saved successfully
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
`;
|
|
||||||
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 = `
|
|
||||||
<i class="fas fa-exclamation-circle me-2"></i>${error.message || 'Failed to save template'}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
`;
|
|
||||||
document.querySelector('.card-body').appendChild(alert);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
// Restore button state
|
|
||||||
saveButton.disabled = false;
|
|
||||||
saveButton.innerHTML = originalText;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Summernote custom styles */
|
/* Summernote custom styles */
|
||||||
|
|||||||
@@ -98,187 +98,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<!-- Load JavaScript -->
|
||||||
// Function to view log details
|
<script src="{{ url_for('static', filename='js/settings/logs.js') }}?v={{ 'js/settings/logs.js'|asset_version }}"></script>
|
||||||
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, '<br>');
|
|
||||||
} else if (typeof details === 'object') {
|
|
||||||
formattedDetails = JSON.stringify(details, null, 2).replace(/\n/g, '<br>');
|
|
||||||
} else {
|
|
||||||
formattedDetails = String(details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content.innerHTML = `
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>Timestamp:</strong> ${new Date(log.timestamp).toLocaleString()}
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>Level:</strong> <span class="badge bg-${getLogLevelBadgeClass(log.level)}">${log.level}</span>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>Category:</strong> ${log.category || '-'}
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>Action:</strong> ${log.action || '-'}
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>Description:</strong> ${log.description || '-'}
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>User:</strong> ${log.user || '-'}
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>IP Address:</strong> ${log.ip_address || '-'}
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>Details:</strong>
|
|
||||||
<div class="mt-2 p-3 bg-light rounded">
|
|
||||||
${formattedDetails}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 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 = `
|
|
||||||
<td>${new Date(log.timestamp).toLocaleString()}</td>
|
|
||||||
<td><span class="badge bg-${getLogLevelBadgeClass(log.level)}">${log.level}</span></td>
|
|
||||||
<td>${log.category || '-'}</td>
|
|
||||||
<td>${log.action || '-'}</td>
|
|
||||||
<td>${log.description || '-'}</td>
|
|
||||||
<td>${log.user || '-'}</td>
|
|
||||||
<td>${log.ip_address || '-'}</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-outline-primary" onclick="viewLogDetails(${log.id})">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
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 = `
|
|
||||||
<button class="page-link" onclick="fetchLogs(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>
|
|
||||||
<i class="fas fa-chevron-left"></i>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
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 = `
|
|
||||||
<button class="page-link" onclick="fetchLogs(${i})">${i}</button>
|
|
||||||
`;
|
|
||||||
pagination.appendChild(li);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next button
|
|
||||||
const nextLi = document.createElement('li');
|
|
||||||
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
|
|
||||||
nextLi.innerHTML = `
|
|
||||||
<button class="page-link" onclick="fetchLogs(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>
|
|
||||||
<i class="fas fa-chevron-right"></i>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
pagination.appendChild(nextLi);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event Listeners
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
@@ -1,311 +1,114 @@
|
|||||||
{% macro smtp_settings_tab(smtp_settings, csrf_token) %}
|
{% macro smtp_settings_tab(smtp_settings, csrf_token) %}
|
||||||
<div class="row">
|
<div class="container-fluid">
|
||||||
<div class="col-12">
|
<form id="smtpSettingsForm" method="POST" action="{{ url_for('settings.save_smtp_settings') }}">
|
||||||
<div class="card shadow-sm">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
|
||||||
|
<!-- SMTP Server Settings -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">SMTP Server Settings</h5>
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="smtpSettingsForm" method="POST" action="{{ url_for('main.update_smtp_settings') }}">
|
<div class="row">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="smtp_host" class="form-label">SMTP Host</label>
|
||||||
<div class="row">
|
<input type="text" class="form-control" id="smtp_host" name="smtp_host" value="{{ smtp_settings.smtp_host }}" required>
|
||||||
<!-- SMTP Server Settings -->
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<h6 class="mb-3">SMTP Server Settings</h6>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smtp_host" class="form-label">
|
|
||||||
SMTP Host
|
|
||||||
<i class="fas fa-info-circle ms-1"
|
|
||||||
style="color: var(--secondary-color); cursor: pointer;"
|
|
||||||
data-bs-toggle="popover"
|
|
||||||
data-bs-html="true"
|
|
||||||
data-bs-title="SMTP Host Information"
|
|
||||||
data-bs-content="
|
|
||||||
<strong>Common SMTP Hosts:</strong><br>
|
|
||||||
• Gmail: smtp.gmail.com<br>
|
|
||||||
• Office 365: smtp.office365.com<br>
|
|
||||||
• Outlook: smtp-mail.outlook.com<br>
|
|
||||||
• Yahoo: smtp.mail.yahoo.com<br>
|
|
||||||
• Zoho: smtp.zoho.com<br>
|
|
||||||
• SendGrid: smtp.sendgrid.net<br>
|
|
||||||
• Amazon SES: email-smtp.[region].amazonaws.com<br>
|
|
||||||
• Custom Domain: mail.yourdomain.com or smtp.yourdomain.com<br><br>
|
|
||||||
<strong>Host Configuration Tips:</strong><br>
|
|
||||||
• Always use the official SMTP server of your email provider<br>
|
|
||||||
• For custom domains, check with your hosting provider<br>
|
|
||||||
• Some providers may require specific subdomains<br>
|
|
||||||
• Ensure the host is accessible from your server's network"
|
|
||||||
style="cursor: pointer;"></i>
|
|
||||||
</label>
|
|
||||||
<input type="text" class="form-control" id="smtp_host" name="smtp_host"
|
|
||||||
value="{{ smtp_settings.smtp_host if smtp_settings else '' }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smtp_port" class="form-label">
|
|
||||||
SMTP Port
|
|
||||||
<i class="fas fa-info-circle ms-1"
|
|
||||||
style="color: var(--secondary-color); cursor: pointer;"
|
|
||||||
data-bs-toggle="popover"
|
|
||||||
data-bs-html="true"
|
|
||||||
data-bs-title="SMTP Port Information"
|
|
||||||
data-bs-content="
|
|
||||||
<strong>Common SMTP Ports:</strong><br>
|
|
||||||
<strong>Port 587 (Recommended):</strong><br>
|
|
||||||
• Default for TLS encryption<br>
|
|
||||||
• Most widely supported<br>
|
|
||||||
• Best for modern email systems<br><br>
|
|
||||||
<strong>Port 465:</strong><br>
|
|
||||||
• Used for SSL encryption<br>
|
|
||||||
• Older but still secure<br>
|
|
||||||
• Some providers prefer this port<br><br>
|
|
||||||
<strong>Port 25:</strong><br>
|
|
||||||
• Traditional SMTP port<br>
|
|
||||||
• Often blocked by ISPs<br>
|
|
||||||
• Not recommended for modern use<br><br>
|
|
||||||
<strong>Port 2525:</strong><br>
|
|
||||||
• Alternative to port 587<br>
|
|
||||||
• Used when port 587 is blocked<br>
|
|
||||||
• Common in cloud environments<br><br>
|
|
||||||
<strong>Port Selection Guidelines:</strong><br>
|
|
||||||
• Always use encrypted ports (587 or 465) in production<br>
|
|
||||||
• Check your email provider's documentation<br>
|
|
||||||
• Some networks may block certain ports<br>
|
|
||||||
• Port 25 is often blocked by ISPs"
|
|
||||||
style="cursor: pointer;"></i>
|
|
||||||
</label>
|
|
||||||
<input type="number" class="form-control" id="smtp_port" name="smtp_port"
|
|
||||||
value="{{ smtp_settings.smtp_port if smtp_settings else '587' }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smtp_security" class="form-label">
|
|
||||||
Security
|
|
||||||
<i class="fas fa-info-circle ms-1"
|
|
||||||
style="color: var(--secondary-color); cursor: pointer;"
|
|
||||||
data-bs-toggle="popover"
|
|
||||||
data-bs-html="true"
|
|
||||||
data-bs-title="Security Best Practices"
|
|
||||||
data-bs-content="
|
|
||||||
<strong>Security Options:</strong><br>
|
|
||||||
<strong>TLS (Recommended):</strong><br>
|
|
||||||
• Uses port 587<br>
|
|
||||||
• Encrypts connection after establishing it<br>
|
|
||||||
• Most widely supported<br><br>
|
|
||||||
<strong>SSL:</strong><br>
|
|
||||||
• Uses port 465<br>
|
|
||||||
• Encrypts connection from the start<br>
|
|
||||||
• Older but still secure<br><br>
|
|
||||||
<strong>None:</strong><br>
|
|
||||||
• Not recommended for production use<br>
|
|
||||||
• May be blocked by many providers<br>
|
|
||||||
• Only use for testing"
|
|
||||||
style="cursor: pointer;"></i>
|
|
||||||
</label>
|
|
||||||
<select class="form-select" id="smtp_security" name="smtp_security">
|
|
||||||
<option value="tls" {% if smtp_settings and smtp_settings.smtp_security == 'tls' %}selected{% endif %}>TLS</option>
|
|
||||||
<option value="ssl" {% if smtp_settings and smtp_settings.smtp_security == 'ssl' %}selected{% endif %}>SSL</option>
|
|
||||||
<option value="none" {% if smtp_settings and smtp_settings.smtp_security == 'none' %}selected{% endif %}>None</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Authentication Settings -->
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<h6 class="mb-3">Authentication</h6>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smtp_username" class="form-label">
|
|
||||||
Username
|
|
||||||
<i class="fas fa-info-circle ms-1"
|
|
||||||
style="color: var(--secondary-color); cursor: pointer;"
|
|
||||||
data-bs-toggle="popover"
|
|
||||||
data-bs-html="true"
|
|
||||||
data-bs-title="Username Guidelines"
|
|
||||||
data-bs-content="
|
|
||||||
<strong>Username Requirements:</strong><br>
|
|
||||||
• For Gmail: Use your full email address<br>
|
|
||||||
• For Office 365: Usually your email address<br>
|
|
||||||
• For custom domains: Check with your email provider<br>
|
|
||||||
• If using 2FA, you may need to generate an App Password<br><br>
|
|
||||||
<strong>Best Practices:</strong><br>
|
|
||||||
• Always use the email address associated with your SMTP account<br>
|
|
||||||
• For shared accounts, use a dedicated service account<br>
|
|
||||||
• Keep track of which username is used for which purpose"
|
|
||||||
style="cursor: pointer;"></i>
|
|
||||||
</label>
|
|
||||||
<input type="text" class="form-control" id="smtp_username" name="smtp_username"
|
|
||||||
value="{{ smtp_settings.smtp_username if smtp_settings else '' }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smtp_password" class="form-label">
|
|
||||||
Password
|
|
||||||
<i class="fas fa-info-circle ms-1"
|
|
||||||
style="color: var(--secondary-color); cursor: pointer;"
|
|
||||||
data-bs-toggle="popover"
|
|
||||||
data-bs-html="true"
|
|
||||||
data-bs-title="Password Security"
|
|
||||||
data-bs-content="
|
|
||||||
<strong>Password Requirements:</strong><br>
|
|
||||||
• For Gmail with 2FA: Use an App Password<br>
|
|
||||||
• For Office 365: May require an App Password if 2FA is enabled<br>
|
|
||||||
• Never use your main account password for SMTP<br>
|
|
||||||
• Regularly rotate your SMTP passwords<br><br>
|
|
||||||
<strong>Security Best Practices:</strong><br>
|
|
||||||
• Use strong, unique passwords<br>
|
|
||||||
• Store passwords securely<br>
|
|
||||||
• Monitor for unauthorized access<br>
|
|
||||||
• Set up alerts for failed login attempts"
|
|
||||||
style="cursor: pointer;"></i>
|
|
||||||
</label>
|
|
||||||
<input type="password" class="form-control" id="smtp_password" name="smtp_password"
|
|
||||||
value="{{ smtp_settings.smtp_password if smtp_settings else '' }}" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sender Settings -->
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<h6 class="mb-3">Sender Information</h6>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smtp_from_email" class="form-label">
|
|
||||||
From Email
|
|
||||||
<i class="fas fa-info-circle ms-1"
|
|
||||||
style="color: var(--secondary-color); cursor: pointer;"
|
|
||||||
data-bs-toggle="popover"
|
|
||||||
data-bs-html="true"
|
|
||||||
data-bs-title="From Email Guidelines"
|
|
||||||
data-bs-content="
|
|
||||||
<strong>Email Requirements:</strong><br>
|
|
||||||
• Must match the domain of your SMTP server<br>
|
|
||||||
• Should be a valid, monitored email address<br>
|
|
||||||
• Use a dedicated sending address (e.g., no-reply@yourdomain.com)<br>
|
|
||||||
• Ensure the domain has proper SPF, DKIM, and DMARC records<br><br>
|
|
||||||
<strong>Best Practices:</strong><br>
|
|
||||||
• Use a consistent sending address<br>
|
|
||||||
• Set up proper email forwarding<br>
|
|
||||||
• Monitor bounce rates and spam reports<br>
|
|
||||||
• Keep the address active and monitored"
|
|
||||||
style="cursor: pointer;"></i>
|
|
||||||
</label>
|
|
||||||
<input type="email" class="form-control" id="smtp_from_email" name="smtp_from_email"
|
|
||||||
value="{{ smtp_settings.smtp_from_email if smtp_settings else '' }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smtp_from_name" class="form-label">
|
|
||||||
From Name
|
|
||||||
<i class="fas fa-info-circle ms-1"
|
|
||||||
style="color: var(--secondary-color); cursor: pointer;"
|
|
||||||
data-bs-toggle="popover"
|
|
||||||
data-bs-html="true"
|
|
||||||
data-bs-title="From Name Best Practices"
|
|
||||||
data-bs-content="
|
|
||||||
<strong>Name Guidelines:</strong><br>
|
|
||||||
• Use your company or application name<br>
|
|
||||||
• Keep it consistent across all emails<br>
|
|
||||||
• Include department name if relevant (e.g., 'DocuPulse Support')<br>
|
|
||||||
• Avoid using personal names unless sending personal communications<br>
|
|
||||||
• Keep it under 50 characters for best display<br><br>
|
|
||||||
<strong>Best Practices:</strong><br>
|
|
||||||
• Make it recognizable to recipients<br>
|
|
||||||
• Avoid special characters<br>
|
|
||||||
• Consider mobile display length<br>
|
|
||||||
• Test how it appears in different email clients"
|
|
||||||
style="cursor: pointer;"></i>
|
|
||||||
</label>
|
|
||||||
<input type="text" class="form-control" id="smtp_from_name" name="smtp_from_name"
|
|
||||||
value="{{ smtp_settings.smtp_from_name if smtp_settings else '' }}" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Email Deliverability Best Practices -->
|
|
||||||
<div class="col-12 mb-4">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<h6 class="alert-heading mb-2">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>Email Deliverability Best Practices
|
|
||||||
</h6>
|
|
||||||
<p class="mb-2">To prevent your emails from being marked as spam, ensure you have:</p>
|
|
||||||
<ul class="mb-0">
|
|
||||||
<li><strong>SPF Record:</strong> Add a TXT record to your domain's DNS settings:
|
|
||||||
<code>v=spf1 include:_spf.your-email-provider.com ~all</code>
|
|
||||||
</li>
|
|
||||||
<li><strong>DKIM:</strong> Add the DKIM record provided by your email provider to your domain's DNS settings</li>
|
|
||||||
<li><strong>DMARC:</strong> Add a TXT record named "_dmarc" with:
|
|
||||||
<code>v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com</code>
|
|
||||||
</li>
|
|
||||||
<li><strong>Reverse DNS (PTR):</strong> Ensure your sending IP has a valid reverse DNS record</li>
|
|
||||||
<li><strong>Domain Age:</strong> Use a domain that's at least 30 days old</li>
|
|
||||||
<li><strong>Email Volume:</strong> Start with a small volume and gradually increase</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
<!-- Test Connection Button -->
|
<label for="smtp_port" class="form-label">SMTP Port</label>
|
||||||
<div class="mb-4">
|
<input type="number" class="form-control" id="smtp_port" name="smtp_port" value="{{ smtp_settings.smtp_port }}" required>
|
||||||
<button type="button" class="btn btn-outline-primary" onclick="testSmtpConnection()">
|
|
||||||
<i class="fas fa-plug me-2"></i>Test Connection
|
|
||||||
</button>
|
|
||||||
<div id="testConnectionResult" class="mt-2"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Save Button -->
|
<div class="row">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="col-md-6 mb-3">
|
||||||
<button type="submit" class="btn btn-primary">
|
<label for="smtp_security" class="form-label">Security</label>
|
||||||
<i class="fas fa-save me-2"></i>Save Settings
|
<select class="form-select" id="smtp_security" name="smtp_security">
|
||||||
</button>
|
<option value="none" {% if smtp_settings.smtp_security == 'none' %}selected{% endif %}>None</option>
|
||||||
|
<option value="tls" {% if smtp_settings.smtp_security == 'tls' %}selected{% endif %}>TLS</option>
|
||||||
|
<option value="ssl" {% if smtp_settings.smtp_security == 'ssl' %}selected{% endif %}>SSL</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<!-- Authentication Settings -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Authentication Settings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="smtp_username" class="form-label">Username</label>
|
||||||
|
<input type="text" class="form-control" id="smtp_username" name="smtp_username" value="{{ smtp_settings.smtp_username }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="smtp_password" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="smtp_password" name="smtp_password" value="{{ smtp_settings.smtp_password }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sender Information -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Sender Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="smtp_from_email" class="form-label">From Email</label>
|
||||||
|
<input type="email" class="form-control" id="smtp_from_email" name="smtp_from_email" value="{{ smtp_settings.smtp_from_email }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="smtp_from_name" class="form-label">From Name</label>
|
||||||
|
<input type="text" class="form-control" id="smtp_from_name" name="smtp_from_name" value="{{ smtp_settings.smtp_from_name }}" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Connection -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Test Connection</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<button type="button" class="btn btn-outline-primary" onclick="testSmtpConnection()">
|
||||||
|
<i class="fas fa-paper-plane me-2"></i>Test Connection
|
||||||
|
</button>
|
||||||
|
<div id="testConnectionResult"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Deliverability Best Practices -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Email Deliverability Best Practices</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="alert-heading">Important Tips for Better Email Deliverability:</h6>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Use a dedicated IP address for sending emails</li>
|
||||||
|
<li>Set up proper SPF, DKIM, and DMARC records</li>
|
||||||
|
<li>Keep your email list clean and up-to-date</li>
|
||||||
|
<li>Monitor your email sending reputation</li>
|
||||||
|
<li>Follow email sending best practices and regulations</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-end mb-4">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save me-2"></i>Save Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script src="{{ url_for('static', filename='js/settings/smtp_settings.js') }}?v={{ 'js/settings/smtp_settings.js'|asset_version }}"></script>
|
||||||
// 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 = '<i class="fas fa-spinner fa-spin me-2"></i>Testing...';
|
|
||||||
|
|
||||||
// Get form data
|
|
||||||
const formData = new FormData(form);
|
|
||||||
|
|
||||||
// Send test request
|
|
||||||
fetch('/settings/test-smtp', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'X-CSRF-Token': '{{ csrf_token }}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(Object.fromEntries(formData))
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
resultDiv.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>Connection successful!</div>';
|
|
||||||
} else {
|
|
||||||
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="fas fa-times-circle me-2"></i>Connection failed: ${data.error}</div>`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="fas fa-times-circle me-2"></i>Error: ${error.message}</div>`;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
// Restore button state
|
|
||||||
button.disabled = false;
|
|
||||||
button.innerHTML = originalText;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
Reference in New Issue
Block a user