diff --git a/ftp/ftpManager.py b/ftp/ftpManager.py index 596bae449..e3fc3404c 100644 --- a/ftp/ftpManager.py +++ b/ftp/ftpManager.py @@ -283,6 +283,7 @@ class FTPManager: 'custom_quota_enabled': items.custom_quota_enabled, 'custom_quota_size': items.custom_quota_size, 'package_quota': items.domain.package.diskSpace, + 'acct_enabled': (str(items.status) == '1'), } if checker == 0: @@ -330,6 +331,75 @@ class FTPManager: json_data = json.dumps(data_ret) return HttpResponse(json_data) + def changeFTPDirectory(self): + """ + Change FTP account home directory after creation. + Uses listFTPAccounts permission (same as password / quota). + """ + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if ACLManager.currentContextPermission(currentACL, 'listFTPAccounts') == 0: + return ACLManager.loadErrorJson('changeDirectoryStatus', 0) + + data = json.loads(self.request.body) + userName = data['ftpUserName'] + selectedDomain = data['selectedDomain'] + newPath = data.get('path', '') + if newPath is None: + newPath = '' + + admin = Administrator.objects.get(pk=userID) + if ACLManager.checkOwnership(selectedDomain, admin, currentACL) != 1: + return ACLManager.loadErrorJson() + + ftp = Users.objects.get(user=userName) + if currentACL['admin'] != 1 and ftp.domain.admin != admin: + return ACLManager.loadErrorJson() + + result = FTPUtilities.changeFTPDirectory(userName, newPath, selectedDomain) + if result[0] == 1: + data_ret = {'status': 1, 'changeDirectoryStatus': 1, 'error_message': 'None'} + else: + data_ret = {'status': 0, 'changeDirectoryStatus': 0, 'error_message': result[1]} + return HttpResponse(json.dumps(data_ret), content_type='application/json') + except BaseException as msg: + data_ret = {'status': 0, 'changeDirectoryStatus': 0, 'error_message': str(msg)} + return HttpResponse(json.dumps(data_ret), content_type='application/json') + + def setFTPAccountStatus(self): + """Enable (enabled=true) or disable (enabled=false) FTP login for a user.""" + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if ACLManager.currentContextPermission(currentACL, 'listFTPAccounts') == 0: + return ACLManager.loadErrorJson('setFTPStatusResult', 0) + + data = json.loads(self.request.body) + userName = data['ftpUserName'] + selectedDomain = data['selectedDomain'] + enabled = bool(data.get('enabled', True)) + + admin = Administrator.objects.get(pk=userID) + if ACLManager.checkOwnership(selectedDomain, admin, currentACL) != 1: + return ACLManager.loadErrorJson() + + ftp = Users.objects.get(user=userName) + if currentACL['admin'] != 1 and ftp.domain.admin != admin: + return ACLManager.loadErrorJson() + + result = FTPUtilities.setFTPAccountStatus(userName, enabled, selectedDomain) + if result[0] == 1: + data_ret = {'status': 1, 'setFTPStatusResult': 1, 'error_message': 'None'} + else: + data_ret = {'status': 0, 'setFTPStatusResult': 0, 'error_message': result[1]} + return HttpResponse(json.dumps(data_ret), content_type='application/json') + except BaseException as msg: + data_ret = {'status': 0, 'setFTPStatusResult': 0, 'error_message': str(msg)} + return HttpResponse(json.dumps(data_ret), content_type='application/json') + def updateFTPQuota(self): try: userID = self.request.session['userID'] diff --git a/ftp/pluginManager.py b/ftp/pluginManager.py index baacfe757..6b2130361 100644 --- a/ftp/pluginManager.py +++ b/ftp/pluginManager.py @@ -33,4 +33,20 @@ class pluginManager: @staticmethod def postChangePassword(request, response): - return pluginManagerGlobal.globalPlug(request, postChangePassword, response) \ No newline at end of file + return pluginManagerGlobal.globalPlug(request, postChangePassword, response) + + @staticmethod + def preChangeFTPDirectory(request): + return pluginManagerGlobal.globalPlug(request, preChangeFTPDirectory) + + @staticmethod + def postChangeFTPDirectory(request, response): + return pluginManagerGlobal.globalPlug(request, postChangeFTPDirectory, response) + + @staticmethod + def preSetFTPAccountStatus(request): + return pluginManagerGlobal.globalPlug(request, preSetFTPAccountStatus) + + @staticmethod + def postSetFTPAccountStatus(request, response): + return pluginManagerGlobal.globalPlug(request, postSetFTPAccountStatus, response) \ No newline at end of file diff --git a/ftp/signals.py b/ftp/signals.py index 874ee0421..6aa74c237 100644 --- a/ftp/signals.py +++ b/ftp/signals.py @@ -26,4 +26,11 @@ postSubmitFTPDelete = Signal() preChangePassword = Signal() ## This event is fired after CyberPanel core finished deletion of child-domain -postChangePassword = Signal() \ No newline at end of file +postChangePassword = Signal() + +## Before / after changing FTP account home directory (list FTP page) +preChangeFTPDirectory = Signal() +postChangeFTPDirectory = Signal() + +preSetFTPAccountStatus = Signal() +postSetFTPAccountStatus = Signal() \ No newline at end of file diff --git a/ftp/static/ftp/ftp.js b/ftp/static/ftp/ftp.js index 47bcfe1ec..83f715a08 100644 --- a/ftp/static/ftp/ftp.js +++ b/ftp/static/ftp/ftp.js @@ -109,15 +109,7 @@ app.controller('createFTPAccount', function ($scope, $http) { $scope.errorMessage = "Invalid path: Path cannot contain '..' or '~'"; return; } - - // Check if path starts with slash (should be relative) - if (path.startsWith("/")) { - $scope.ftpLoading = false; - resetFtpCreateAlerts(); - $scope.alertFtpCreateError = true; - $scope.errorMessage = "Invalid path: Path must be relative (not starting with '/')"; - return; - } + // Absolute paths under /home/... are allowed; server validates they stay inside the site home } var url = "/ftp/submitFTPCreation"; @@ -375,6 +367,7 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpAccounts = true; $scope.changePasswordBox = true; $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.notificationsBox = true; var globalFTPUsername = ""; @@ -390,6 +383,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.couldNotConnect = true; $scope.ftpLoading = false; // Don't show loading when opening password dialog $scope.changePasswordBox = false; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.notificationsBox = true; $scope.ftpUsername = ftpUsername; globalFTPUsername = ftpUsername; @@ -454,6 +449,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = true; // Show loading while fetching $scope.ftpAccounts = true; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; var selectedDomain = $scope.selectedDomain; @@ -479,7 +476,11 @@ app.controller('listFTPAccounts', function ($scope, $http) { if (response.data.fetchStatus == 1) { $scope.records = JSON.parse(response.data.data); - + angular.forEach($scope.records, function (r) { + if (typeof r.acct_enabled === 'undefined') { + r.acct_enabled = true; + } + }); $scope.notificationsBox = false; $scope.recordsFetched = false; @@ -489,6 +490,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = false; // Hide loading when done $scope.ftpAccounts = false; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.domainFeteched = $scope.selectedDomain; @@ -501,6 +504,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = false; // Hide loading on error $scope.ftpAccounts = true; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.errorMessage = response.data.error_message; } @@ -516,6 +521,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = false; // Hide loading on connection error $scope.ftpAccounts = true; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; } @@ -542,6 +549,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.canNotChangePassword = true; $scope.couldNotConnect = true; $scope.ftpLoading = false; + $scope.changePasswordBox = true; + $scope.directoryManagementBox = true; $scope.quotaManagementBox = false; $scope.notificationsBox = true; $scope.ftpUsername = record.user; @@ -630,6 +639,128 @@ app.controller('listFTPAccounts', function ($scope, $http) { } }; + $scope.manageDirectory = function (record) { + $scope.recordsFetched = true; + $scope.passwordChanged = true; + $scope.canNotChangePassword = true; + $scope.couldNotConnect = true; + $scope.ftpLoading = false; + $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = false; + $scope.notificationsBox = true; + $scope.ftpUsername = record.user; + globalFTPUsername = record.user; + $scope.ftpPathEdit = record.dir || ''; + }; + + $scope.changeDirectoryBtn = function () { + $scope.ftpLoading = true; + var url = "/ftp/changeFTPDirectory"; + var pathVal = $scope.ftpPathEdit; + if (typeof pathVal === 'undefined' || pathVal === null) { + pathVal = ''; + } else { + pathVal = String(pathVal).trim(); + } + var data = { + ftpUserName: globalFTPUsername, + selectedDomain: $scope.selectedDomain, + path: pathVal + }; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + + function ListInitialDatas(response) { + if (response.data.changeDirectoryStatus === 1) { + $scope.ftpLoading = false; + $scope.directoryManagementBox = true; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: 'FTP directory updated successfully.', + type: 'success' + }); + } + populateCurrentRecords(); + } else { + $scope.ftpLoading = false; + $scope.errorMessage = response.data.error_message; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: response.data.error_message, + type: 'error' + }); + } + } + } + + function cantLoadInitialDatas() { + $scope.ftpLoading = false; + $scope.couldNotConnect = false; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: 'Could not connect to server.', + type: 'error' + }); + } + } + }; + + $scope.setFtpAccountStatus = function (record, enabled) { + $scope.ftpLoading = true; + var url = "/ftp/setFTPAccountStatus"; + var data = { + ftpUserName: record.user, + selectedDomain: $scope.selectedDomain, + enabled: !!enabled + }; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + $http.post(url, data, config).then(function (response) { + $scope.ftpLoading = false; + if (response.data.setFTPStatusResult === 1) { + record.acct_enabled = !!enabled; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: enabled ? 'FTP account enabled.' : 'FTP account disabled.', + type: 'success' + }); + } + populateCurrentRecords(); + } else { + $scope.errorMessage = response.data.error_message; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: response.data.error_message, + type: 'error' + }); + } + } + }, function () { + $scope.ftpLoading = false; + $scope.couldNotConnect = false; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: 'Could not connect to server.', + type: 'error' + }); + } + }); + }; + }); diff --git a/ftp/templates/ftp/createFTPAccount.html b/ftp/templates/ftp/createFTPAccount.html index dd494125f..0850528c7 100644 --- a/ftp/templates/ftp/createFTPAccount.html +++ b/ftp/templates/ftp/createFTPAccount.html @@ -569,26 +569,26 @@
- +
- {% trans "Leave empty to use the website's home directory, or specify a subdirectory" %} + {% trans "Leave empty for the website's home directory. Use a subdirectory, or the full path under that home." %}
- {% trans "Examples:" %} {% trans "docs, public_html, uploads, api" %} + {% trans "Examples:" %} {% trans "public_html, uploads, or /home/yourdomain.com/public_html" %}
- {% trans "Security: Path will be restricted to this subdirectory only" %} + {% trans "Security: FTP will be restricted to this directory only (must stay inside the site home)" %}
- - {% trans "Do not use: .. ~ / or special characters like ; | & $ ` ' \" < > * ?" %} + {% trans "Do not use: .. ~ or special characters like ; | & $ ` ' \" < > * ?" %}
diff --git a/ftp/templates/ftp/listFTPAccounts.html b/ftp/templates/ftp/listFTPAccounts.html index 97a7b25b2..6f7dffaae 100644 --- a/ftp/templates/ftp/listFTPAccounts.html +++ b/ftp/templates/ftp/listFTPAccounts.html @@ -301,6 +301,10 @@ font-size: 0.875rem; font-weight: 500; } + + .ftp-row-disabled td { + opacity: 0.75; + } @keyframes spin { to { transform: rotate(360deg); } @@ -541,25 +545,58 @@ + +
+

{% trans "FTP home directory for" %} {$ ftpUsername $}

+
+
+
+ +
+ + {% trans "Leave empty to use this site's home directory. You may enter a subdirectory or the full path under that home (no path traversal)." %} +
+ +
+
+ +
+
+
+
+
- - - - - + + + + + + - + + diff --git a/ftp/urls.py b/ftp/urls.py index 19adb1764..e4cdebfb0 100644 --- a/ftp/urls.py +++ b/ftp/urls.py @@ -15,6 +15,8 @@ urlpatterns = [ path('listFTPAccounts', views.listFTPAccounts, name='listFTPAccounts'), path('getAllFTPAccounts', views.getAllFTPAccounts, name='getAllFTPAccounts'), path('changePassword', views.changePassword, name='changePassword'), + path('changeFTPDirectory', views.changeFTPDirectory, name='changeFTPDirectory'), + path('setFTPAccountStatus', views.setFTPAccountStatus, name='setFTPAccountStatus'), path('updateFTPQuota', views.updateFTPQuota, name='updateFTPQuota'), path('getFTPQuotaUsage', views.getFTPQuotaUsage, name='getFTPQuotaUsage'), path('migrateFTPQuotas', views.migrateFTPQuotas, name='migrateFTPQuotas'), diff --git a/ftp/views.py b/ftp/views.py index f7f2b088b..f42fc6468 100644 --- a/ftp/views.py +++ b/ftp/views.py @@ -255,6 +255,42 @@ def changePassword(request): except KeyError: return redirect(loadLoginPage) +def changeFTPDirectory(request): + try: + result = pluginManager.preChangeFTPDirectory(request) + if result != 200: + return result + + fm = FTPManager(request) + coreResult = fm.changeFTPDirectory() + + result = pluginManager.postChangeFTPDirectory(request, coreResult) + if result != 200: + return result + + return coreResult + + except KeyError: + return redirect(loadLoginPage) + +def setFTPAccountStatus(request): + try: + result = pluginManager.preSetFTPAccountStatus(request) + if result != 200: + return result + + fm = FTPManager(request) + coreResult = fm.setFTPAccountStatus() + + result = pluginManager.postSetFTPAccountStatus(request, coreResult) + if result != 200: + return result + + return coreResult + + except KeyError: + return redirect(loadLoginPage) + def updateFTPQuota(request): try: fm = FTPManager(request) diff --git a/install/pure-ftpd-one/pureftpd-mysql.conf b/install/pure-ftpd-one/pureftpd-mysql.conf index f882c9600..befb00bba 100644 --- a/install/pure-ftpd-one/pureftpd-mysql.conf +++ b/install/pure-ftpd-one/pureftpd-mysql.conf @@ -3,12 +3,12 @@ MYSQLPort 3306 MYSQLSocket /var/lib/mysql/mysql.sock MYSQLDatabase cyberpanel MYSQLCrypt md5 -MYSQLGetDir SELECT Dir FROM users WHERE User='\L' -MYSQLGetGID SELECT Gid FROM users WHERE User='\L' -MYSQLGetPW SELECT Password FROM users WHERE User='\L' -MYSQLGetUID SELECT Uid FROM users WHERE User='\L' -# Quota enforcement queries -MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L' -MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L' +# Only accounts with Status='1' can authenticate (disabled = Status '0') +MYSQLGetDir SELECT Dir FROM users WHERE User='\L' AND Status='1' +MYSQLGetGID SELECT Gid FROM users WHERE User='\L' AND Status='1' +MYSQLGetPW SELECT Password FROM users WHERE User='\L' AND Status='1' +MYSQLGetUID SELECT Uid FROM users WHERE User='\L' AND Status='1' +MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L' AND Status='1' +MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L' AND Status='1' MYSQLPassword 1qaz@9xvps MYSQLUser cyberpanel diff --git a/install/pure-ftpd/pureftpd-mysql.conf b/install/pure-ftpd/pureftpd-mysql.conf index f2e5548f0..2dd1ad2f8 100644 --- a/install/pure-ftpd/pureftpd-mysql.conf +++ b/install/pure-ftpd/pureftpd-mysql.conf @@ -3,12 +3,13 @@ MYSQLPort 3307 MYSQLSocket /var/lib/mysql1/mysql.sock MYSQLDatabase cyberpanel MYSQLCrypt md5 -MYSQLGetDir SELECT Dir FROM users WHERE User='\L' -MYSQLGetGID SELECT Gid FROM users WHERE User='\L' -MYSQLGetPW SELECT Password FROM users WHERE User='\L' -MYSQLGetUID SELECT Uid FROM users WHERE User='\L' +# Only accounts with Status='1' can authenticate (disabled = Status '0') +MYSQLGetDir SELECT Dir FROM users WHERE User='\L' AND Status='1' +MYSQLGetGID SELECT Gid FROM users WHERE User='\L' AND Status='1' +MYSQLGetPW SELECT Password FROM users WHERE User='\L' AND Status='1' +MYSQLGetUID SELECT Uid FROM users WHERE User='\L' AND Status='1' # Quota enforcement queries -MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L' -MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L' +MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L' AND Status='1' +MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L' AND Status='1' MYSQLPassword 1qaz@9xvps MYSQLUser cyberpanel diff --git a/plogical/ftpUtilities.py b/plogical/ftpUtilities.py index d2be496a5..e4f3693fa 100644 --- a/plogical/ftpUtilities.py +++ b/plogical/ftpUtilities.py @@ -22,6 +22,57 @@ from plogical.processUtilities import ProcessUtilities class FTPUtilities: + @staticmethod + def get_domain_home_directory(domain_name): + """ + Filesystem root for the selected site: primary domain /home/, + or child domain /home// per CyberPanel vhost layout. + """ + try: + child = ChildDomains.objects.select_related('master').get(domain=domain_name) + master_dom = child.master.domain + rel = (child.path or '').strip().strip('/') + if rel: + return os.path.abspath('/home/%s/%s' % (master_dom, rel)) + except ChildDomains.DoesNotExist: + pass + return os.path.abspath('/home/' + domain_name) + + @staticmethod + def assert_ftp_raw_path_safe(raw): + """Reject shell metacharacters and obvious traversal markers in user input.""" + if raw is None or not str(raw).strip(): + return + s = str(raw) + dangerous_chars = [';', '|', '&', '$', '`', '\'', '"', '<', '>', '*', '?'] + if any(char in s for char in dangerous_chars): + raise BaseException("Invalid path: Path contains dangerous characters") + if '..' in s or '~' in s: + raise BaseException("Invalid path: Path cannot contain '..' or '~'") + + @staticmethod + def resolve_ftp_home_path(domain_name, raw_path): + """ + Resolve FTP home directory under domain_name. + Empty / None / 'None' -> domain document root only. + Absolute paths are allowed if they resolve under that root (no /home duplication). + """ + domain_home = FTPUtilities.get_domain_home_directory(domain_name) + if raw_path is None: + return domain_home + raw = str(raw_path).strip() + if raw == '' or raw == 'None': + return domain_home + FTPUtilities.assert_ftp_raw_path_safe(raw) + if raw.startswith('/'): + candidate = os.path.abspath(raw) + else: + candidate = os.path.abspath(os.path.join(domain_home, raw)) + dh = domain_home + if candidate != dh and not candidate.startswith(dh + os.sep): + raise BaseException("Security violation: Path must be within domain home directory") + return candidate + @staticmethod def createNewFTPAccount(udb,upass,username,password,path): try: @@ -143,37 +194,14 @@ class FTPUtilities: ## gid , uid ends - # Enhanced path validation and handling - if path and path.strip() and path != 'None': - # Clean the path - path = path.strip().lstrip("/") - - # Additional security checks - if path.find("..") > -1 or path.find("~") > -1 or path.startswith("/"): - raise BaseException("Invalid path: Path must be relative and not contain '..' or '~' or start with '/'") - - # Check for dangerous characters - dangerous_chars = [';', '|', '&', '$', '`', '\'', '"', '<', '>', '*', '?'] - if any(char in path for char in dangerous_chars): - raise BaseException("Invalid path: Path contains dangerous characters") - - # Construct full path - full_path = "/home/" + domainName + "/" + path - - # Additional security: ensure path is within domain directory - domain_home = "/home/" + domainName - if not os.path.abspath(full_path).startswith(os.path.abspath(domain_home)): - raise BaseException("Security violation: Path must be within domain directory") - - result = FTPUtilities.ftpFunctions(full_path, externalApp) - - if result[0] == 1: - path = full_path - else: + # Path: empty -> domain home; relative or absolute under domain home (no duplicate /home/... prefix) + if path and str(path).strip() and str(path).strip() != 'None': + path = FTPUtilities.resolve_ftp_home_path(domainName, path) + result = FTPUtilities.ftpFunctions(path, externalApp) + if result[0] != 1: raise BaseException("Path validation failed: " + result[1]) - else: - path = "/home/" + domainName + path = FTPUtilities.get_domain_home_directory(domainName) # Enhanced symlink handling if os.path.islink(path): @@ -251,6 +279,63 @@ class FTPUtilities: except BaseException as msg: return 0, str(msg) + @staticmethod + def changeFTPDirectory(userName, raw_path, selected_domain): + """ + Update FTP user home directory after creation. selected_domain must match + the master website domain for this account (same as list FTP dropdown). + """ + try: + website = Websites.objects.get(domain=selected_domain) + ftp = Users.objects.get(user=userName) + if ftp.domain_id != website.id: + raise BaseException("FTP user does not belong to the selected domain") + + externalApp = website.externalApp + resolved = FTPUtilities.resolve_ftp_home_path(selected_domain, raw_path) + + if os.path.islink(resolved): + logging.CyberCPLogFileWriter.writeToFile( + "FTP path is symlinked: %s" % resolved) + raise BaseException("Cannot set FTP directory: Path is a symbolic link") + + result = FTPUtilities.ftpFunctions(resolved, externalApp) + if result[0] != 1: + raise BaseException("Path validation failed: " + result[1]) + + ftp.dir = resolved + ftp.save() + return 1, None + except Users.DoesNotExist: + return 0, "FTP user not found" + except Websites.DoesNotExist: + return 0, "Domain not found" + except BaseException as msg: + logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [changeFTPDirectory]") + return 0, str(msg) + + @staticmethod + def setFTPAccountStatus(userName, enabled, selected_domain): + """ + Enable or disable FTP login (Status '1' / '0'). Pure-FTPd must use + MySQL queries that include AND Status='1' for authentication. + """ + try: + website = Websites.objects.get(domain=selected_domain) + ftp = Users.objects.get(user=userName) + if ftp.domain_id != website.id: + raise BaseException("FTP user does not belong to the selected domain") + ftp.status = '1' if enabled else '0' + ftp.save() + return 1, None + except Users.DoesNotExist: + return 0, "FTP user not found" + except Websites.DoesNotExist: + return 0, "Domain not found" + except BaseException as msg: + logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [setFTPAccountStatus]") + return 0, str(msg) + @staticmethod def changeFTPPassword(userName, password): try: diff --git a/public/static/ftp/ftp.js b/public/static/ftp/ftp.js index 47bcfe1ec..1071d554a 100644 --- a/public/static/ftp/ftp.js +++ b/public/static/ftp/ftp.js @@ -109,15 +109,7 @@ app.controller('createFTPAccount', function ($scope, $http) { $scope.errorMessage = "Invalid path: Path cannot contain '..' or '~'"; return; } - - // Check if path starts with slash (should be relative) - if (path.startsWith("/")) { - $scope.ftpLoading = false; - resetFtpCreateAlerts(); - $scope.alertFtpCreateError = true; - $scope.errorMessage = "Invalid path: Path must be relative (not starting with '/')"; - return; - } + // Absolute paths under /home/... are allowed; server validates they stay inside the site home } var url = "/ftp/submitFTPCreation"; @@ -375,6 +367,7 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpAccounts = true; $scope.changePasswordBox = true; $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.notificationsBox = true; var globalFTPUsername = ""; @@ -390,6 +383,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.couldNotConnect = true; $scope.ftpLoading = false; // Don't show loading when opening password dialog $scope.changePasswordBox = false; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.notificationsBox = true; $scope.ftpUsername = ftpUsername; globalFTPUsername = ftpUsername; @@ -454,6 +449,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = true; // Show loading while fetching $scope.ftpAccounts = true; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; var selectedDomain = $scope.selectedDomain; @@ -479,7 +476,11 @@ app.controller('listFTPAccounts', function ($scope, $http) { if (response.data.fetchStatus == 1) { $scope.records = JSON.parse(response.data.data); - + angular.forEach($scope.records, function (r) { + if (typeof r.acct_enabled === 'undefined') { + r.acct_enabled = true; + } + }); $scope.notificationsBox = false; $scope.recordsFetched = false; @@ -489,6 +490,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = false; // Hide loading when done $scope.ftpAccounts = false; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.domainFeteched = $scope.selectedDomain; @@ -501,6 +504,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = false; // Hide loading on error $scope.ftpAccounts = true; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.errorMessage = response.data.error_message; } @@ -516,6 +521,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = false; // Hide loading on connection error $scope.ftpAccounts = true; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; } @@ -542,6 +549,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.canNotChangePassword = true; $scope.couldNotConnect = true; $scope.ftpLoading = false; + $scope.changePasswordBox = true; + $scope.directoryManagementBox = true; $scope.quotaManagementBox = false; $scope.notificationsBox = true; $scope.ftpUsername = record.user; @@ -630,6 +639,80 @@ app.controller('listFTPAccounts', function ($scope, $http) { } }; + $scope.manageDirectory = function (record) { + $scope.recordsFetched = true; + $scope.passwordChanged = true; + $scope.canNotChangePassword = true; + $scope.couldNotConnect = true; + $scope.ftpLoading = false; + $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = false; + $scope.notificationsBox = true; + $scope.ftpUsername = record.user; + globalFTPUsername = record.user; + $scope.ftpPathEdit = record.dir || ''; + }; + + $scope.changeDirectoryBtn = function () { + $scope.ftpLoading = true; + var url = "/ftp/changeFTPDirectory"; + var pathVal = $scope.ftpPathEdit; + if (typeof pathVal === 'undefined' || pathVal === null) { + pathVal = ''; + } else { + pathVal = String(pathVal).trim(); + } + var data = { + ftpUserName: globalFTPUsername, + selectedDomain: $scope.selectedDomain, + path: pathVal + }; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + + function ListInitialDatas(response) { + if (response.data.changeDirectoryStatus === 1) { + $scope.ftpLoading = false; + $scope.directoryManagementBox = true; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: 'FTP directory updated successfully.', + type: 'success' + }); + } + populateCurrentRecords(); + } else { + $scope.ftpLoading = false; + $scope.errorMessage = response.data.error_message; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: response.data.error_message, + type: 'error' + }); + } + } + } + + function cantLoadInitialDatas() { + $scope.ftpLoading = false; + $scope.couldNotConnect = false; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: 'Could not connect to server.', + type: 'error' + }); + } + } + }; + }); diff --git a/static/ftp/ftp.js b/static/ftp/ftp.js index 47bcfe1ec..1071d554a 100644 --- a/static/ftp/ftp.js +++ b/static/ftp/ftp.js @@ -109,15 +109,7 @@ app.controller('createFTPAccount', function ($scope, $http) { $scope.errorMessage = "Invalid path: Path cannot contain '..' or '~'"; return; } - - // Check if path starts with slash (should be relative) - if (path.startsWith("/")) { - $scope.ftpLoading = false; - resetFtpCreateAlerts(); - $scope.alertFtpCreateError = true; - $scope.errorMessage = "Invalid path: Path must be relative (not starting with '/')"; - return; - } + // Absolute paths under /home/... are allowed; server validates they stay inside the site home } var url = "/ftp/submitFTPCreation"; @@ -375,6 +367,7 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpAccounts = true; $scope.changePasswordBox = true; $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.notificationsBox = true; var globalFTPUsername = ""; @@ -390,6 +383,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.couldNotConnect = true; $scope.ftpLoading = false; // Don't show loading when opening password dialog $scope.changePasswordBox = false; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.notificationsBox = true; $scope.ftpUsername = ftpUsername; globalFTPUsername = ftpUsername; @@ -454,6 +449,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = true; // Show loading while fetching $scope.ftpAccounts = true; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; var selectedDomain = $scope.selectedDomain; @@ -479,7 +476,11 @@ app.controller('listFTPAccounts', function ($scope, $http) { if (response.data.fetchStatus == 1) { $scope.records = JSON.parse(response.data.data); - + angular.forEach($scope.records, function (r) { + if (typeof r.acct_enabled === 'undefined') { + r.acct_enabled = true; + } + }); $scope.notificationsBox = false; $scope.recordsFetched = false; @@ -489,6 +490,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = false; // Hide loading when done $scope.ftpAccounts = false; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.domainFeteched = $scope.selectedDomain; @@ -501,6 +504,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = false; // Hide loading on error $scope.ftpAccounts = true; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; $scope.errorMessage = response.data.error_message; } @@ -516,6 +521,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.ftpLoading = false; // Hide loading on connection error $scope.ftpAccounts = true; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = true; } @@ -542,6 +549,8 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.canNotChangePassword = true; $scope.couldNotConnect = true; $scope.ftpLoading = false; + $scope.changePasswordBox = true; + $scope.directoryManagementBox = true; $scope.quotaManagementBox = false; $scope.notificationsBox = true; $scope.ftpUsername = record.user; @@ -630,6 +639,80 @@ app.controller('listFTPAccounts', function ($scope, $http) { } }; + $scope.manageDirectory = function (record) { + $scope.recordsFetched = true; + $scope.passwordChanged = true; + $scope.canNotChangePassword = true; + $scope.couldNotConnect = true; + $scope.ftpLoading = false; + $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; + $scope.directoryManagementBox = false; + $scope.notificationsBox = true; + $scope.ftpUsername = record.user; + globalFTPUsername = record.user; + $scope.ftpPathEdit = record.dir || ''; + }; + + $scope.changeDirectoryBtn = function () { + $scope.ftpLoading = true; + var url = "/ftp/changeFTPDirectory"; + var pathVal = $scope.ftpPathEdit; + if (typeof pathVal === 'undefined' || pathVal === null) { + pathVal = ''; + } else { + pathVal = String(pathVal).trim(); + } + var data = { + ftpUserName: globalFTPUsername, + selectedDomain: $scope.selectedDomain, + path: pathVal + }; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + + function ListInitialDatas(response) { + if (response.data.changeDirectoryStatus === 1) { + $scope.ftpLoading = false; + $scope.directoryManagementBox = true; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: 'FTP directory updated successfully.', + type: 'success' + }); + } + populateCurrentRecords(); + } else { + $scope.ftpLoading = false; + $scope.errorMessage = response.data.error_message; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: response.data.error_message, + type: 'error' + }); + } + } + } + + function cantLoadInitialDatas() { + $scope.ftpLoading = false; + $scope.couldNotConnect = false; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: 'Could not connect to server.', + type: 'error' + }); + } + } + }; + });
{% trans "ID" %}{% trans "User Name" %}{% trans "Directory" %}{% trans "Quota" %}{% trans "Actions" %}{% trans "ID" %}{% trans "User Name" %}{% trans "Status" %}{% trans "Directory" %}{% trans "Quota" %}{% trans "Actions" %}
+ + {% trans "Enabled" %} + + + {% trans "Disabled" %} + + @@ -579,6 +616,18 @@ {% trans "Quota" %} + + +