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