mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-06 10:55:45 +02:00
CyberPanel: fix webmail folder selection and UI assets
This commit is contained in:
BIN
bin/lswsgi
BIN
bin/lswsgi
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 33 KiB |
File diff suppressed because one or more lines are too long
@@ -270,7 +270,7 @@ p {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
|
||||
.table-responsive {
|
||||
border: none;
|
||||
margin-bottom: 20px;
|
||||
@@ -284,19 +284,19 @@ p {
|
||||
max-width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
/* Adjust sidebar for tablets */
|
||||
#page-sidebar {
|
||||
width: 100%;
|
||||
position: static;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
/* Make tables horizontally scrollable */
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
|
||||
.table {
|
||||
min-width: 600px;
|
||||
}
|
||||
@@ -307,17 +307,17 @@ p {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
.container, .container-fluid {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* Stack all columns on mobile */
|
||||
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12,
|
||||
.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 {
|
||||
@@ -325,24 +325,24 @@ p {
|
||||
max-width: 100%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
|
||||
/* Adjust headings for mobile */
|
||||
h1 {
|
||||
font-size: 2rem !important; /* 32px */
|
||||
}
|
||||
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem !important; /* 28px */
|
||||
}
|
||||
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem !important; /* 24px */
|
||||
}
|
||||
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem !important; /* 20px */
|
||||
}
|
||||
|
||||
|
||||
/* Button adjustments for mobile */
|
||||
.btn {
|
||||
font-size: 16px !important;
|
||||
@@ -350,50 +350,50 @@ p {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.btn-group .btn {
|
||||
width: auto;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Form adjustments for mobile */
|
||||
.form-control, input, textarea, select {
|
||||
font-size: 16px !important; /* Prevents zoom on iOS */
|
||||
padding: 14px 16px !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* Table adjustments for mobile */
|
||||
.table {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
|
||||
.table th, .table td {
|
||||
padding: 8px 6px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
|
||||
/* Hide less important columns on mobile */
|
||||
.table .d-none-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Modal adjustments for mobile */
|
||||
.modal-dialog {
|
||||
margin: 10px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
|
||||
.modal-content {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
|
||||
/* Content box adjustments */
|
||||
.content-box, .panel, .card {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
|
||||
/* Sidebar adjustments for mobile */
|
||||
#page-sidebar {
|
||||
position: fixed;
|
||||
@@ -406,20 +406,20 @@ p {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
|
||||
#page-sidebar.show {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Main content adjustments when sidebar is open */
|
||||
#main-content {
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
#main-content.sidebar-open {
|
||||
margin-left: 280px;
|
||||
}
|
||||
|
||||
|
||||
/* Mobile menu toggle */
|
||||
.mobile-menu-toggle {
|
||||
display: block;
|
||||
@@ -442,29 +442,29 @@ p {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.container, .container-fluid {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
|
||||
/* Even smaller buttons and forms for very small screens */
|
||||
.btn {
|
||||
font-size: 14px !important;
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
|
||||
.form-control, input, textarea, select {
|
||||
font-size: 16px !important; /* Still 16px to prevent zoom */
|
||||
padding: 12px 14px !important;
|
||||
}
|
||||
|
||||
|
||||
/* Compact table for very small screens */
|
||||
.table th, .table td {
|
||||
padding: 6px 4px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
|
||||
/* Hide even more columns on very small screens */
|
||||
.table .d-none-mobile-sm {
|
||||
display: none !important;
|
||||
@@ -577,12 +577,12 @@ span, div, p, label, td, th {
|
||||
color: #000000 !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
|
||||
.table th, .table td {
|
||||
font-size: 10pt !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
|
||||
.btn, .alert, .modal {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ span, div, p, label, td, th, a, li {
|
||||
.container-fluid {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
|
||||
/* Fix mobile menu text */
|
||||
.mobile-menu .menu-item,
|
||||
.mobile-menu .menu-item span {
|
||||
@@ -233,7 +233,7 @@ span, div, p, label, td, th, a, li {
|
||||
color: #000000 !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
|
||||
.text-muted,
|
||||
.text-secondary {
|
||||
color: #000000 !important;
|
||||
@@ -247,7 +247,7 @@ span, div, p, label, td, th, a, li {
|
||||
--text-secondary: #000000;
|
||||
--text-heading: #000000;
|
||||
}
|
||||
|
||||
|
||||
[data-theme="dark"] {
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #ffffff;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2032,10 +2032,18 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
var comodoInstalled = false;
|
||||
var counterOWASP = 0;
|
||||
var counterComodo = 0;
|
||||
var updatingOWASPStatus = false;
|
||||
var updatingComodoStatus = false;
|
||||
|
||||
|
||||
$('#owaspInstalled').change(function () {
|
||||
|
||||
// Prevent triggering installation when status check updates the toggle
|
||||
if (updatingOWASPStatus) {
|
||||
counterOWASP = counterOWASP + 1; // Still increment counter
|
||||
return;
|
||||
}
|
||||
|
||||
owaspInstalled = $(this).prop('checked');
|
||||
$scope.ruleFiles = true;
|
||||
|
||||
@@ -2052,6 +2060,12 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
$('#comodoInstalled').change(function () {
|
||||
|
||||
// Prevent triggering installation when status check updates the toggle
|
||||
if (updatingComodoStatus) {
|
||||
counterComodo = counterComodo + 1; // Still increment counter
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.ruleFiles = true;
|
||||
comodoInstalled = $(this).prop('checked');
|
||||
|
||||
@@ -2070,9 +2084,12 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
|
||||
getOWASPAndComodoStatus(true);
|
||||
function getOWASPAndComodoStatus(updateToggle) {
|
||||
function getOWASPAndComodoStatus(updateToggle, showLoader) {
|
||||
|
||||
$scope.modsecLoading = false;
|
||||
// Only show loader if explicitly requested (during installations)
|
||||
if (showLoader === true) {
|
||||
$scope.modsecLoading = false;
|
||||
}
|
||||
|
||||
|
||||
url = "/firewall/getOWASPAndComodoStatus";
|
||||
@@ -2097,6 +2114,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
if (updateToggle === true) {
|
||||
|
||||
// Set flags to prevent change event from triggering installation
|
||||
updatingOWASPStatus = true;
|
||||
updatingComodoStatus = true;
|
||||
|
||||
if (response.data.owaspInstalled === 1) {
|
||||
$('#owaspInstalled').prop('checked', true);
|
||||
$scope.owaspDisable = false;
|
||||
@@ -2115,6 +2136,7 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
$scope.comodoDisable = true;
|
||||
comodoInstalled = false;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if (response.data.owaspInstalled === 1) {
|
||||
@@ -2135,10 +2157,19 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
}
|
||||
|
||||
// Always reset flags after status check completes
|
||||
$timeout(function() {
|
||||
updatingOWASPStatus = false;
|
||||
updatingComodoStatus = false;
|
||||
}, 100);
|
||||
|
||||
}
|
||||
|
||||
function cantLoadInitialDatas(response) {
|
||||
$scope.modsecLoading = true;
|
||||
// Reset flags even on error
|
||||
updatingOWASPStatus = false;
|
||||
updatingComodoStatus = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2180,7 +2211,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
$scope.installationFailed = true;
|
||||
$scope.installationSuccess = false;
|
||||
|
||||
getOWASPAndComodoStatus(false);
|
||||
// Update toggle state after a short delay to reflect installation result
|
||||
$timeout(function() {
|
||||
getOWASPAndComodoStatus(true);
|
||||
}, 500);
|
||||
|
||||
} else {
|
||||
$scope.modsecLoading = true;
|
||||
@@ -2193,6 +2227,11 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
$scope.installationSuccess = true;
|
||||
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
|
||||
// Update toggle to reflect failed installation (will show OFF)
|
||||
$timeout(function() {
|
||||
getOWASPAndComodoStatus(true);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -118,8 +118,8 @@ class ServerStatusUtil(multi.Thread):
|
||||
if ServerStatusUtil.executioner(command, statusFile) == 0:
|
||||
return 0
|
||||
|
||||
if os.path.exists('/usr/local/CyberCP/lsws-6.0/'):
|
||||
shutil.rmtree('/usr/local/CyberCP/lsws-6.0')
|
||||
if os.path.exists('/usr/local/CyberCP/lsws-6.3.4/'):
|
||||
shutil.rmtree('/usr/local/CyberCP/lsws-6.3.4')
|
||||
|
||||
if os.path.exists(f'/usr/local/CyberCP/lsws-{lsws_version}/'):
|
||||
shutil.rmtree(f'/usr/local/CyberCP/lsws-{lsws_version}/')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2713,4 +2713,3 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2032,10 +2032,18 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
var comodoInstalled = false;
|
||||
var counterOWASP = 0;
|
||||
var counterComodo = 0;
|
||||
var updatingOWASPStatus = false;
|
||||
var updatingComodoStatus = false;
|
||||
|
||||
|
||||
$('#owaspInstalled').change(function () {
|
||||
|
||||
// Prevent triggering installation when status check updates the toggle
|
||||
if (updatingOWASPStatus) {
|
||||
counterOWASP = counterOWASP + 1; // Still increment counter
|
||||
return;
|
||||
}
|
||||
|
||||
owaspInstalled = $(this).prop('checked');
|
||||
$scope.ruleFiles = true;
|
||||
|
||||
@@ -2052,6 +2060,12 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
$('#comodoInstalled').change(function () {
|
||||
|
||||
// Prevent triggering installation when status check updates the toggle
|
||||
if (updatingComodoStatus) {
|
||||
counterComodo = counterComodo + 1; // Still increment counter
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.ruleFiles = true;
|
||||
comodoInstalled = $(this).prop('checked');
|
||||
|
||||
@@ -2070,9 +2084,12 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
|
||||
getOWASPAndComodoStatus(true);
|
||||
function getOWASPAndComodoStatus(updateToggle) {
|
||||
function getOWASPAndComodoStatus(updateToggle, showLoader) {
|
||||
|
||||
$scope.modsecLoading = false;
|
||||
// Only show loader if explicitly requested (during installations)
|
||||
if (showLoader === true) {
|
||||
$scope.modsecLoading = false;
|
||||
}
|
||||
|
||||
|
||||
url = "/firewall/getOWASPAndComodoStatus";
|
||||
@@ -2097,6 +2114,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
if (updateToggle === true) {
|
||||
|
||||
// Set flags to prevent change event from triggering installation
|
||||
updatingOWASPStatus = true;
|
||||
updatingComodoStatus = true;
|
||||
|
||||
if (response.data.owaspInstalled === 1) {
|
||||
$('#owaspInstalled').prop('checked', true);
|
||||
$scope.owaspDisable = false;
|
||||
@@ -2115,6 +2136,7 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
$scope.comodoDisable = true;
|
||||
comodoInstalled = false;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if (response.data.owaspInstalled === 1) {
|
||||
@@ -2135,10 +2157,19 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
}
|
||||
|
||||
// Always reset flags after status check completes
|
||||
$timeout(function() {
|
||||
updatingOWASPStatus = false;
|
||||
updatingComodoStatus = false;
|
||||
}, 100);
|
||||
|
||||
}
|
||||
|
||||
function cantLoadInitialDatas(response) {
|
||||
$scope.modsecLoading = true;
|
||||
// Reset flags even on error
|
||||
updatingOWASPStatus = false;
|
||||
updatingComodoStatus = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2180,7 +2211,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
$scope.installationFailed = true;
|
||||
$scope.installationSuccess = false;
|
||||
|
||||
getOWASPAndComodoStatus(false);
|
||||
// Update toggle state after a short delay to reflect installation result
|
||||
$timeout(function() {
|
||||
getOWASPAndComodoStatus(true);
|
||||
}, 500);
|
||||
|
||||
} else {
|
||||
$scope.modsecLoading = true;
|
||||
@@ -2193,6 +2227,11 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
|
||||
$scope.installationSuccess = true;
|
||||
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
|
||||
// Update toggle to reflect failed installation (will show OFF)
|
||||
$timeout(function() {
|
||||
getOWASPAndComodoStatus(true);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ class CyberPanelWebAuthn {
|
||||
this.apiEndpoints = {
|
||||
registrationStart: '/webauthn/registration/start/',
|
||||
registrationComplete: '/webauthn/registration/complete/',
|
||||
authenticationOptions: '/webauthn/authentication/options/',
|
||||
authenticationStart: '/webauthn/authentication/start/',
|
||||
authenticationComplete: '/webauthn/authentication/complete/',
|
||||
credentialsList: '/webauthn/credentials/',
|
||||
@@ -60,18 +61,10 @@ class CyberPanelWebAuthn {
|
||||
addLoginButtons() {
|
||||
const loginForm = document.querySelector('#loginForm');
|
||||
if (!loginForm) return;
|
||||
|
||||
// Add WebAuthn login button
|
||||
const webauthnButton = document.createElement('button');
|
||||
webauthnButton.type = 'button';
|
||||
webauthnButton.className = 'btn btn-primary btn-block';
|
||||
webauthnButton.innerHTML = '<i class="fas fa-fingerprint"></i> Login with Passkey';
|
||||
webauthnButton.onclick = () => this.startPasswordlessLogin();
|
||||
|
||||
// Insert after password field
|
||||
const passwordField = loginForm.querySelector('input[type="password"]');
|
||||
if (passwordField) {
|
||||
passwordField.parentNode.insertBefore(webauthnButton, passwordField.parentNode.nextSibling);
|
||||
const existingBtn = document.getElementById('webauthn-login-btn');
|
||||
if (existingBtn && !existingBtn.dataset.bound) {
|
||||
existingBtn.dataset.bound = '1';
|
||||
existingBtn.onclick = () => this.startPasskeyFirstLogin();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +73,76 @@ class CyberPanelWebAuthn {
|
||||
// Implementation depends on the specific UI structure
|
||||
}
|
||||
|
||||
arrayBufferToBase64url(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
base64urlToArrayBuffer(str) {
|
||||
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const pad = (4 - (base64.length % 4)) % 4;
|
||||
for (let i = 0; i < pad; i++) base64 += '=';
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
async startPasskeyFirstLogin() {
|
||||
try {
|
||||
this.showLoading('Signing in with passkey...');
|
||||
const optsUrl = this.apiEndpoints.authenticationOptions + '?return=' + encodeURIComponent(window.location.pathname || '/');
|
||||
const optsResponse = await fetch(optsUrl, { method: 'GET', credentials: 'same-origin' });
|
||||
const optsData = await optsResponse.json();
|
||||
if (!optsData.publicKey) {
|
||||
throw new Error(optsData.error || 'Failed to get options');
|
||||
}
|
||||
const publicKey = optsData.publicKey;
|
||||
publicKey.challenge = this.base64urlToArrayBuffer(publicKey.challenge);
|
||||
if (publicKey.allowCredentials && publicKey.allowCredentials.length) {
|
||||
publicKey.allowCredentials = publicKey.allowCredentials.map(function(c) {
|
||||
return {
|
||||
type: c.type || 'public-key',
|
||||
id: typeof c.id === 'string' ? this.base64urlToArrayBuffer(c.id) : c.id,
|
||||
transports: c.transports
|
||||
};
|
||||
}.bind(this));
|
||||
}
|
||||
const credential = await navigator.credentials.get({ publicKey });
|
||||
if (!credential) throw new Error('No credential');
|
||||
const credentialJson = {
|
||||
id: credential.id,
|
||||
rawId: this.arrayBufferToBase64url(credential.rawId),
|
||||
type: credential.type,
|
||||
response: {
|
||||
clientDataJSON: this.arrayBufferToBase64url(credential.response.clientDataJSON),
|
||||
authenticatorData: this.arrayBufferToBase64url(credential.response.authenticatorData),
|
||||
signature: this.arrayBufferToBase64url(credential.response.signature),
|
||||
userHandle: credential.response.userHandle ? this.arrayBufferToBase64url(credential.response.userHandle) : null
|
||||
}
|
||||
};
|
||||
const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, {
|
||||
credential: credentialJson
|
||||
});
|
||||
if (authResponse.success && authResponse.redirect) {
|
||||
window.location.href = authResponse.redirect;
|
||||
return;
|
||||
}
|
||||
throw new Error(authResponse.error || 'Verification failed');
|
||||
} catch (error) {
|
||||
if (error.name === 'NotAllowedError' || (error.message && (error.message.indexOf('cancel') !== -1 || error.message.indexOf('timed out') !== -1))) {
|
||||
this.hideLoading();
|
||||
return;
|
||||
}
|
||||
console.error('WebAuthn passkey-first error:', error);
|
||||
this.showError(error.message || 'Passkey sign-in failed');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async startPasswordlessLogin() {
|
||||
try {
|
||||
const username = document.querySelector('input[name="username"]').value;
|
||||
@@ -87,27 +150,13 @@ class CyberPanelWebAuthn {
|
||||
this.showError('Please enter your username first');
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLoading('Starting passkey authentication...');
|
||||
|
||||
// Get authentication challenge
|
||||
const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationStart, {
|
||||
username: username
|
||||
});
|
||||
|
||||
const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationStart, { username: username });
|
||||
if (!challengeResponse.success) {
|
||||
throw new Error(challengeResponse.error || 'Failed to start authentication');
|
||||
}
|
||||
|
||||
// Convert challenge to proper format
|
||||
const challenge = this.convertChallenge(challengeResponse.challenge);
|
||||
|
||||
// Get credential
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: challenge
|
||||
});
|
||||
|
||||
// Complete authentication
|
||||
const credential = await navigator.credentials.get({ publicKey: challenge });
|
||||
const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, {
|
||||
challenge_id: challengeResponse.challenge_id,
|
||||
credential: {
|
||||
@@ -117,19 +166,14 @@ class CyberPanelWebAuthn {
|
||||
client_data_json: this.arrayBufferToBase64(credential.response.clientDataJSON),
|
||||
authenticator_data: this.arrayBufferToBase64(credential.response.authenticatorData),
|
||||
signature: this.arrayBufferToBase64(credential.response.signature),
|
||||
user_handle: credential.response.userHandle ?
|
||||
this.arrayBufferToBase64(credential.response.userHandle) : null
|
||||
user_handle: credential.response.userHandle ? this.arrayBufferToBase64(credential.response.userHandle) : null
|
||||
});
|
||||
|
||||
if (authResponse.success) {
|
||||
this.showSuccess('Authentication successful! Redirecting...');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
setTimeout(() => { window.location.href = authResponse.redirect || '/'; }, 1000);
|
||||
} else {
|
||||
throw new Error(authResponse.error || 'Authentication failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('WebAuthn authentication error:', error);
|
||||
this.showError(error.message || 'Authentication failed');
|
||||
@@ -138,9 +182,10 @@ class CyberPanelWebAuthn {
|
||||
}
|
||||
}
|
||||
|
||||
async registerPasskey(username, credentialName = '') {
|
||||
async registerPasskey(username, credentialName = '', options = {}) {
|
||||
const silent = options && options.silent === true;
|
||||
try {
|
||||
this.showLoading('Starting passkey registration...');
|
||||
if (!silent) this.showLoading('Starting passkey registration...');
|
||||
|
||||
// Get registration challenge
|
||||
const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.registrationStart, {
|
||||
@@ -172,7 +217,7 @@ class CyberPanelWebAuthn {
|
||||
});
|
||||
|
||||
if (regResponse.success) {
|
||||
this.showSuccess('Passkey registered successfully!');
|
||||
if (!silent) this.showSuccess('Passkey registered successfully!');
|
||||
return regResponse;
|
||||
} else {
|
||||
throw new Error(regResponse.error || 'Registration failed');
|
||||
@@ -180,10 +225,10 @@ class CyberPanelWebAuthn {
|
||||
|
||||
} catch (error) {
|
||||
console.error('WebAuthn registration error:', error);
|
||||
this.showError(error.message || 'Registration failed');
|
||||
if (!silent) this.showError(error.message || 'Registration failed');
|
||||
throw error;
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
if (!silent) this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,23 +310,25 @@ class CyberPanelWebAuthn {
|
||||
}
|
||||
|
||||
convertChallenge(challenge) {
|
||||
// Convert base64 challenge to ArrayBuffer
|
||||
const challengeBytes = this.base64ToArrayBuffer(challenge.challenge);
|
||||
|
||||
const ch = challenge.challenge;
|
||||
const challengeBytes = (typeof ch === 'string' && (ch.indexOf('-') !== -1 || ch.indexOf('_') !== -1))
|
||||
? this.base64urlToArrayBuffer(ch) : this.base64ToArrayBuffer(ch);
|
||||
const userId = challenge.user && challenge.user.id;
|
||||
const userIdBuf = !userId ? undefined : (typeof userId === 'string' && (userId.indexOf('-') !== -1 || userId.indexOf('_') !== -1)
|
||||
? this.base64urlToArrayBuffer(userId) : this.base64ToArrayBuffer(userId));
|
||||
return {
|
||||
...challenge,
|
||||
challenge: challengeBytes,
|
||||
user: {
|
||||
...challenge.user,
|
||||
id: this.base64ToArrayBuffer(challenge.user.id)
|
||||
},
|
||||
user: challenge.user ? { ...challenge.user, id: userIdBuf } : undefined,
|
||||
excludeCredentials: challenge.excludeCredentials?.map(cred => ({
|
||||
...cred,
|
||||
id: this.base64ToArrayBuffer(cred.id)
|
||||
id: typeof cred.id === 'string' && (cred.id.indexOf('-') !== -1 || cred.id.indexOf('_') !== -1)
|
||||
? this.base64urlToArrayBuffer(cred.id) : this.base64ToArrayBuffer(cred.id)
|
||||
})) || [],
|
||||
allowCredentials: challenge.allowCredentials?.map(cred => ({
|
||||
...cred,
|
||||
id: this.base64ToArrayBuffer(cred.id)
|
||||
id: typeof cred.id === 'string' && (cred.id.indexOf('-') !== -1 || cred.id.indexOf('_') !== -1)
|
||||
? this.base64urlToArrayBuffer(cred.id) : this.base64ToArrayBuffer(cred.id)
|
||||
})) || []
|
||||
};
|
||||
}
|
||||
@@ -383,12 +430,17 @@ class CyberPanelWebAuthn {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize WebAuthn when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize WebAuthn - run now if DOM ready, else on DOMContentLoaded (script often loads after DOM is ready)
|
||||
function initCyberPanelWebAuthn() {
|
||||
if (CyberPanelWebAuthn.isSupported()) {
|
||||
window.cyberPanelWebAuthn = new CyberPanelWebAuthn();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initCyberPanelWebAuthn);
|
||||
} else {
|
||||
initCyberPanelWebAuthn();
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
|
||||
@@ -1619,3 +1619,341 @@ app.controller('EmailLimitsNew', function ($scope, $http) {
|
||||
|
||||
});
|
||||
/* Java script for EmailLimitsNew */
|
||||
|
||||
/* Catch-All Email Controller */
|
||||
app.controller('catchAllEmail', function ($scope, $http) {
|
||||
|
||||
$scope.configBox = true;
|
||||
$scope.loading = false;
|
||||
$scope.errorBox = true;
|
||||
$scope.successBox = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
$scope.currentConfigured = false;
|
||||
$scope.enabled = true;
|
||||
|
||||
$scope.fetchConfig = function () {
|
||||
if (!$scope.selectedDomain) {
|
||||
$scope.configBox = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.configBox = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var url = "/email/fetchCatchAllConfig";
|
||||
var data = { domain: $scope.selectedDomain };
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(function (response) {
|
||||
$scope.loading = false;
|
||||
if (response.data.fetchStatus === 1) {
|
||||
$scope.configBox = false;
|
||||
if (response.data.configured === 1) {
|
||||
$scope.currentConfigured = true;
|
||||
$scope.currentDestination = response.data.destination;
|
||||
$scope.currentEnabled = response.data.enabled;
|
||||
$scope.destination = response.data.destination;
|
||||
$scope.enabled = response.data.enabled;
|
||||
} else {
|
||||
$scope.currentConfigured = false;
|
||||
$scope.destination = '';
|
||||
$scope.enabled = true;
|
||||
}
|
||||
} else {
|
||||
$scope.errorBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
}
|
||||
}, function (response) {
|
||||
$scope.loading = false;
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.notifyBox = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.saveConfig = function () {
|
||||
if (!$scope.destination) {
|
||||
$scope.errorBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.errorMessage = 'Please enter a destination email address';
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var url = "/email/saveCatchAllConfig";
|
||||
var data = {
|
||||
domain: $scope.selectedDomain,
|
||||
destination: $scope.destination,
|
||||
enabled: $scope.enabled
|
||||
};
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(function (response) {
|
||||
$scope.loading = false;
|
||||
if (response.data.saveStatus === 1) {
|
||||
$scope.successBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.successMessage = response.data.message;
|
||||
$scope.currentConfigured = true;
|
||||
$scope.currentDestination = $scope.destination;
|
||||
$scope.currentEnabled = $scope.enabled;
|
||||
} else {
|
||||
$scope.errorBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
}
|
||||
}, function (response) {
|
||||
$scope.loading = false;
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.notifyBox = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteConfig = function () {
|
||||
if (!confirm('Are you sure you want to remove the catch-all configuration?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var url = "/email/deleteCatchAllConfig";
|
||||
var data = { domain: $scope.selectedDomain };
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(function (response) {
|
||||
$scope.loading = false;
|
||||
if (response.data.deleteStatus === 1) {
|
||||
$scope.successBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.successMessage = response.data.message;
|
||||
$scope.currentConfigured = false;
|
||||
$scope.destination = '';
|
||||
$scope.enabled = true;
|
||||
} else {
|
||||
$scope.errorBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
}
|
||||
}, function (response) {
|
||||
$scope.loading = false;
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.notifyBox = false;
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
/* Plus-Addressing Controller */
|
||||
app.controller('plusAddressing', function ($scope, $http) {
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.globalEnabled = false;
|
||||
$scope.delimiter = '+';
|
||||
$scope.domainEnabled = true;
|
||||
$scope.globalNotifyBox = true;
|
||||
$scope.globalErrorBox = true;
|
||||
$scope.globalSuccessBox = true;
|
||||
$scope.domainNotifyBox = true;
|
||||
$scope.domainErrorBox = true;
|
||||
$scope.domainSuccessBox = true;
|
||||
|
||||
// Fetch global settings on load
|
||||
var url = "/email/fetchPlusAddressingConfig";
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, {}, config).then(function (response) {
|
||||
$scope.loading = false;
|
||||
if (response.data.fetchStatus === 1) {
|
||||
$scope.globalEnabled = response.data.globalEnabled;
|
||||
$scope.delimiter = response.data.delimiter || '+';
|
||||
}
|
||||
}, function (response) {
|
||||
$scope.loading = false;
|
||||
});
|
||||
|
||||
$scope.saveGlobalSettings = function () {
|
||||
$scope.loading = true;
|
||||
$scope.globalNotifyBox = true;
|
||||
|
||||
var url = "/email/savePlusAddressingGlobal";
|
||||
var data = {
|
||||
enabled: $scope.globalEnabled,
|
||||
delimiter: $scope.delimiter
|
||||
};
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(function (response) {
|
||||
$scope.loading = false;
|
||||
if (response.data.saveStatus === 1) {
|
||||
$scope.globalSuccessBox = false;
|
||||
$scope.globalNotifyBox = false;
|
||||
$scope.globalSuccessMessage = response.data.message;
|
||||
} else {
|
||||
$scope.globalErrorBox = false;
|
||||
$scope.globalNotifyBox = false;
|
||||
$scope.globalErrorMessage = response.data.error_message;
|
||||
}
|
||||
}, function (response) {
|
||||
$scope.loading = false;
|
||||
$scope.globalErrorBox = false;
|
||||
$scope.globalNotifyBox = false;
|
||||
$scope.globalErrorMessage = 'Could not connect to server';
|
||||
});
|
||||
};
|
||||
|
||||
$scope.saveDomainSettings = function () {
|
||||
if (!$scope.selectedDomain) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.domainNotifyBox = true;
|
||||
|
||||
var url = "/email/savePlusAddressingDomain";
|
||||
var data = {
|
||||
domain: $scope.selectedDomain,
|
||||
enabled: $scope.domainEnabled
|
||||
};
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(function (response) {
|
||||
if (response.data.saveStatus === 1) {
|
||||
$scope.domainSuccessBox = false;
|
||||
$scope.domainNotifyBox = false;
|
||||
$scope.domainSuccessMessage = response.data.message;
|
||||
} else {
|
||||
$scope.domainErrorBox = false;
|
||||
$scope.domainNotifyBox = false;
|
||||
$scope.domainErrorMessage = response.data.error_message;
|
||||
}
|
||||
}, function (response) {
|
||||
$scope.domainErrorBox = false;
|
||||
$scope.domainNotifyBox = false;
|
||||
$scope.domainErrorMessage = 'Could not connect to server';
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
/* Pattern Forwarding Controller */
|
||||
app.controller('patternForwarding', function ($scope, $http) {
|
||||
|
||||
$scope.configBox = true;
|
||||
$scope.loading = false;
|
||||
$scope.errorBox = true;
|
||||
$scope.successBox = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
$scope.rules = [];
|
||||
$scope.patternType = 'wildcard';
|
||||
$scope.priority = 100;
|
||||
|
||||
$scope.fetchRules = function () {
|
||||
if (!$scope.selectedDomain) {
|
||||
$scope.configBox = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.configBox = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var url = "/email/fetchPatternRules";
|
||||
var data = { domain: $scope.selectedDomain };
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(function (response) {
|
||||
$scope.loading = false;
|
||||
if (response.data.fetchStatus === 1) {
|
||||
$scope.configBox = false;
|
||||
$scope.rules = response.data.rules;
|
||||
} else {
|
||||
$scope.errorBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
}
|
||||
}, function (response) {
|
||||
$scope.loading = false;
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.notifyBox = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createRule = function () {
|
||||
if (!$scope.pattern || !$scope.destination) {
|
||||
$scope.errorBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.errorMessage = 'Please enter both pattern and destination';
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var url = "/email/createPatternRule";
|
||||
var data = {
|
||||
domain: $scope.selectedDomain,
|
||||
pattern: $scope.pattern,
|
||||
destination: $scope.destination,
|
||||
pattern_type: $scope.patternType,
|
||||
priority: $scope.priority
|
||||
};
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(function (response) {
|
||||
$scope.loading = false;
|
||||
if (response.data.createStatus === 1) {
|
||||
$scope.successBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.successMessage = response.data.message;
|
||||
$scope.pattern = '';
|
||||
$scope.destination = '';
|
||||
$scope.fetchRules();
|
||||
} else {
|
||||
$scope.errorBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
}
|
||||
}, function (response) {
|
||||
$scope.loading = false;
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.notifyBox = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteRule = function (ruleId) {
|
||||
if (!confirm('Are you sure you want to delete this forwarding rule?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var url = "/email/deletePatternRule";
|
||||
var data = { ruleId: ruleId };
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(function (response) {
|
||||
$scope.loading = false;
|
||||
if (response.data.deleteStatus === 1) {
|
||||
$scope.successBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.successMessage = response.data.message;
|
||||
$scope.fetchRules();
|
||||
} else {
|
||||
$scope.errorBox = false;
|
||||
$scope.notifyBox = false;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
}
|
||||
}, function (response) {
|
||||
$scope.loading = false;
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.notifyBox = false;
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
@@ -64,7 +64,15 @@ app.controller('createPackage', function ($scope, $http) {
|
||||
dataBases: dataBases,
|
||||
emails: emails,
|
||||
allowedDomains: $scope.allowedDomains,
|
||||
enforceDiskLimits: $scope.enforceDiskLimits
|
||||
enforceDiskLimits: $scope.enforceDiskLimits,
|
||||
// Resource Limits
|
||||
memoryLimitMB: $scope.memoryLimitMB || 1024,
|
||||
cpuCores: $scope.cpuCores || 1,
|
||||
ioLimitMBPS: $scope.ioLimitMBPS || 10,
|
||||
inodeLimit: $scope.inodeLimit || 400000,
|
||||
maxConnections: $scope.maxConnections || 10,
|
||||
procSoftLimit: $scope.procSoftLimit || 400,
|
||||
procHardLimit: $scope.procHardLimit || 500
|
||||
};
|
||||
|
||||
var config = {
|
||||
@@ -236,6 +244,15 @@ app.controller('modifyPackages', function ($scope, $http) {
|
||||
$scope.allowFullDomain = response.data.allowFullDomain === 1;
|
||||
$scope.enforceDiskLimits = response.data.enforceDiskLimits === 1;
|
||||
|
||||
// Load resource limits
|
||||
$scope.memoryLimitMB = response.data.memoryLimitMB || 1024;
|
||||
$scope.cpuCores = response.data.cpuCores || 1;
|
||||
$scope.ioLimitMBPS = response.data.ioLimitMBPS || 10;
|
||||
$scope.inodeLimit = response.data.inodeLimit || 400000;
|
||||
$scope.maxConnections = response.data.maxConnections || 10;
|
||||
$scope.procSoftLimit = response.data.procSoftLimit || 400;
|
||||
$scope.procHardLimit = response.data.procHardLimit || 500;
|
||||
|
||||
$scope.modifyButton = "Save Details";
|
||||
|
||||
$("#packageDetailsToBeModified").fadeIn();
|
||||
@@ -283,6 +300,14 @@ app.controller('modifyPackages', function ($scope, $http) {
|
||||
allowedDomains: $scope.allowedDomains,
|
||||
allowFullDomain: $scope.allowFullDomain,
|
||||
enforceDiskLimits: $scope.enforceDiskLimits,
|
||||
// Resource Limits
|
||||
memoryLimitMB: $scope.memoryLimitMB || 1024,
|
||||
cpuCores: $scope.cpuCores || 1,
|
||||
ioLimitMBPS: $scope.ioLimitMBPS || 10,
|
||||
inodeLimit: $scope.inodeLimit || 400000,
|
||||
maxConnections: $scope.maxConnections || 10,
|
||||
procSoftLimit: $scope.procSoftLimit || 400,
|
||||
procHardLimit: $scope.procHardLimit || 500
|
||||
};
|
||||
|
||||
var config = {
|
||||
|
||||
@@ -215,6 +215,58 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', function($
|
||||
apiCall('/webmail/api/listFolders', {}, function(data) {
|
||||
if (data.status === 1) {
|
||||
$scope.folders = data.folders;
|
||||
// Pick a sane default folder.
|
||||
// Some Dovecot setups may not expose a real "INBOX" mailbox (messages live under "INBOX.*").
|
||||
// The UI previously hardcoded currentFolder='INBOX', which caused "No messages" even when mail exists.
|
||||
var chooseDefaultFolder = function(folders) {
|
||||
if (!folders || folders.length === 0) return 'INBOX';
|
||||
|
||||
// 1) Prefer exact INBOX if it has messages; otherwise some servers store mail only under INBOX.*.
|
||||
var inbox = null;
|
||||
for (var i = 0; i < folders.length; i++) {
|
||||
if (folders[i] && folders[i].name === 'INBOX') {
|
||||
inbox = folders[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (inbox) {
|
||||
var inboxUnread = parseInt((inbox.unread_count) ? inbox.unread_count : 0, 10);
|
||||
var inboxTotal = parseInt((inbox.total_count) ? inbox.total_count : 0, 10);
|
||||
if (inboxUnread > 0 || inboxTotal > 0) {
|
||||
return inbox.name;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Prefer the folder with most unread messages.
|
||||
var best = null;
|
||||
var bestUnread = -1;
|
||||
for (var j = 0; j < folders.length; j++) {
|
||||
var u = parseInt((folders[j] && folders[j].unread_count) ? folders[j].unread_count : 0, 10);
|
||||
if (u > bestUnread) {
|
||||
bestUnread = u;
|
||||
best = folders[j];
|
||||
}
|
||||
}
|
||||
if (best && bestUnread > 0) {
|
||||
return best.name;
|
||||
}
|
||||
|
||||
// 3) Otherwise, pick the folder with most total messages.
|
||||
best = null;
|
||||
var bestTotal = -1;
|
||||
for (var k = 0; k < folders.length; k++) {
|
||||
var t = parseInt((folders[k] && folders[k].total_count) ? folders[k].total_count : 0, 10);
|
||||
if (t > bestTotal) {
|
||||
bestTotal = t;
|
||||
best = folders[k];
|
||||
}
|
||||
}
|
||||
|
||||
return (best && best.name) ? best.name : (folders[0].name || 'INBOX');
|
||||
};
|
||||
|
||||
$scope.currentFolder = chooseDefaultFolder($scope.folders);
|
||||
$scope.currentPage = 1;
|
||||
$scope.loadMessages();
|
||||
} else {
|
||||
notify(data.error_message || 'Failed to load folders.', 'error');
|
||||
|
||||
Reference in New Issue
Block a user