diff --git a/app/portainer/react/components/settings.ts b/app/portainer/react/components/settings.ts index ce83d0aa9..2f9f3a4ee 100644 --- a/app/portainer/react/components/settings.ts +++ b/app/portainer/react/components/settings.ts @@ -10,6 +10,7 @@ import { ApplicationSettingsPanel } from '@/react/portainer/settings/SettingsVie import { KubeSettingsPanel } from '@/react/portainer/settings/SettingsView/KubeSettingsPanel'; import { HelmCertPanel } from '@/react/portainer/settings/SettingsView/HelmCertPanel'; import { HiddenContainersPanel } from '@/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersPanel'; +import { SSLSettingsPanelWrapper } from '@/react/portainer/settings/SettingsView/SSLSettingsPanel/SSLSettingsPanel'; export const settingsModule = angular .module('portainer.app.react.components.settings', []) @@ -26,6 +27,10 @@ export const settingsModule = angular 'applicationSettingsPanel', r2a(withReactQuery(ApplicationSettingsPanel), ['onSuccess']) ) + .component( + 'sslSettingsPanel', + r2a(withReactQuery(SSLSettingsPanelWrapper), []) + ) .component('helmCertPanel', r2a(withReactQuery(HelmCertPanel), [])) .component( 'hiddenContainersPanel', diff --git a/app/portainer/settings/general/index.js b/app/portainer/settings/general/index.js index ab9158702..82d992c80 100644 --- a/app/portainer/settings/general/index.js +++ b/app/portainer/settings/general/index.js @@ -1,5 +1,3 @@ import angular from 'angular'; -import { sslCertificate } from './ssl-certificate'; - -export default angular.module('portainer.settings.general', []).component('sslCertificateSettings', sslCertificate).name; +export default angular.module('portainer.settings.general', []).name; diff --git a/app/portainer/settings/general/ssl-certificate/index.js b/app/portainer/settings/general/ssl-certificate/index.js deleted file mode 100644 index 0d5d62af7..000000000 --- a/app/portainer/settings/general/ssl-certificate/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import controller from './ssl-certificate.controller.js'; - -export const sslCertificate = { - templateUrl: './ssl-certificate.html', - controller, -}; diff --git a/app/portainer/settings/general/ssl-certificate/ssl-certificate.controller.js b/app/portainer/settings/general/ssl-certificate/ssl-certificate.controller.js deleted file mode 100644 index c5dfd4dda..000000000 --- a/app/portainer/settings/general/ssl-certificate/ssl-certificate.controller.js +++ /dev/null @@ -1,78 +0,0 @@ -class SslCertificateController { - /* @ngInject */ - constructor($async, $scope, $state, SSLService, Notifications) { - Object.assign(this, { $async, $scope, $state, SSLService, Notifications }); - - this.cert = null; - this.originalValues = { - forceHTTPS: false, - certFile: null, - keyFile: null, - }; - - this.formValues = { - certFile: null, - keyFile: null, - forceHTTPS: false, - }; - - this.state = { - actionInProgress: false, - reloadingPage: false, - }; - - this.certFilePattern = `.pem,.crt,.cer,.cert`; - this.keyFilePattern = `.pem,.key`; - - this.save = this.save.bind(this); - this.onChangeForceHTTPS = this.onChangeForceHTTPS.bind(this); - } - - isFormChanged() { - return Object.entries(this.originalValues).some(([key, value]) => value != this.formValues[key]); - } - - onChangeForceHTTPS(checked) { - return this.$scope.$evalAsync(() => { - this.formValues.forceHTTPS = checked; - }); - } - - async save() { - return this.$async(async () => { - this.state.actionInProgress = true; - try { - const cert = this.formValues.certFile ? await this.formValues.certFile.text() : null; - const key = this.formValues.keyFile ? await this.formValues.keyFile.text() : null; - const httpEnabled = !this.formValues.forceHTTPS; - await this.SSLService.upload({ httpEnabled, cert, key }); - - await new Promise((resolve) => setTimeout(resolve, 2000)); - location.reload(); - this.state.reloadingPage = true; - } catch (err) { - this.Notifications.error('Failure', err, 'Failed applying changes'); - } - this.state.actionInProgress = false; - }); - } - - wasHTTPsChanged() { - return this.originalValues.forceHTTPS !== this.formValues.forceHTTPS; - } - - async $onInit() { - return this.$async(async () => { - try { - const certInfo = await this.SSLService.get(); - - this.formValues.forceHTTPS = !certInfo.httpEnabled; - this.originalValues.forceHTTPS = this.formValues.forceHTTPS; - } catch (err) { - this.Notifications.error('Failure', err, 'Failed loading certificate info'); - } - }); - } -} - -export default SslCertificateController; diff --git a/app/portainer/settings/general/ssl-certificate/ssl-certificate.html b/app/portainer/settings/general/ssl-certificate/ssl-certificate.html deleted file mode 100644 index 921068e14..000000000 --- a/app/portainer/settings/general/ssl-certificate/ssl-certificate.html +++ /dev/null @@ -1,121 +0,0 @@ -
-
- - - -
- -

- - Forcing HTTPs only will cause Portainer to stop listening on the HTTP port. Any edge agent environment that is using HTTP will no longer be available. -

-
-
- -
- - - - Provide a new SSL Certificate to replace the existing one that is used for HTTPS connections. - - - -
-
- - SSL/TLS certificate - - - - - {{ $ctrl.formValues.certFile.name }} - - -
-
-
-
-
-

- - File type is invalid.

-
-
-
- - -
-
- - SSL/TLS private key - - - - - {{ $ctrl.formValues.keyFile.name }} - - -
-
-
-
-
-

- - File type is invalid.

-
-
-
- -
-
- - {{ state.formValidationError }} -
-
-
-
-
-
-
diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index d1a558ee0..9adca482f 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -6,7 +6,7 @@ - + diff --git a/app/react/components/form-components/FileUpload/FileUploadField.tsx b/app/react/components/form-components/FileUpload/FileUploadField.tsx index 33d583dac..6bbed545d 100644 --- a/app/react/components/form-components/FileUpload/FileUploadField.tsx +++ b/app/react/components/form-components/FileUpload/FileUploadField.tsx @@ -30,7 +30,7 @@ export function FileUploadField({ const fileRef = createRef(); return ( -
+
@@ -116,6 +117,6 @@ function validation(): SchemaOf { 'crt', 'cer', 'cert', - ]).required(), + ]).required(''), }); } diff --git a/app/react/portainer/settings/SettingsView/SSLSettingsPanel/SSLSettingsPanel.tsx b/app/react/portainer/settings/SettingsView/SSLSettingsPanel/SSLSettingsPanel.tsx new file mode 100644 index 000000000..66013d5e5 --- /dev/null +++ b/app/react/portainer/settings/SettingsView/SSLSettingsPanel/SSLSettingsPanel.tsx @@ -0,0 +1,169 @@ +import { Form, Formik } from 'formik'; +import { Key } from 'lucide-react'; +import { useState } from 'react'; +import { SchemaOf, bool, object } from 'yup'; + +import { withHideOnExtension } from '@/react/hooks/withHideOnExtension'; + +import { Widget } from '@@/Widget'; +import { LoadingButton } from '@@/buttons'; +import { + file, + withFileExtension, +} from '@@/form-components/yup-file-validation'; +import { TextTip } from '@@/Tip/TextTip'; +import { FormControl } from '@@/form-components/FormControl'; +import { FileUploadField } from '@@/form-components/FileUpload'; +import { SwitchField } from '@@/form-components/SwitchField'; + +import { useUpdateSSLConfigMutation } from '../useUpdateSSLConfigMutation'; +import { useSSLSettings } from '../../queries/useSSLSettings'; + +interface FormValues { + certFile: File | null; + keyFile: File | null; + forceHTTPS: boolean; +} + +export const SSLSettingsPanelWrapper = withHideOnExtension(SSLSettingsPanel); + +function SSLSettingsPanel() { + const settingsQuery = useSSLSettings(); + const [reloadingPage, setReloadingPage] = useState(false); + const mutation = useUpdateSSLConfigMutation(); + + if (!settingsQuery.data) { + return null; + } + + const initialValues: FormValues = { + certFile: null, + keyFile: null, + forceHTTPS: !settingsQuery.data.httpEnabled, + }; + + return ( +
+
+ + + + + {({ values, setFieldValue, isValid, errors }) => ( +
+
+
+ + Forcing HTTPs only will cause Portainer to stop + listening on the HTTP port. Any edge agent environment + that is using HTTP will no longer be available. + +
+
+ +
+
+ setFieldValue('forceHTTPS', value)} + /> +
+
+ +
+
+ + Provide a new SSL Certificate to replace the existing + one that is used for HTTPS connections. + +
+
+ + + setFieldValue('certFile', file)} + value={values.certFile} + /> + + + + setFieldValue('keyFile', file)} + value={values.keyFile} + /> + + +
+
+ + Save SSL Settings + +
+
+
+ )} +
+
+
+
+
+ ); + + function handleSubmit({ certFile, forceHTTPS, keyFile }: FormValues) { + if (!certFile || !keyFile) { + return; + } + + mutation.mutate( + { certFile, httpEnabled: !forceHTTPS, keyFile }, + { + async onSuccess() { + await new Promise((resolve) => { + setTimeout(resolve, 5000); + }); + window.location.reload(); + setReloadingPage(true); + }, + } + ); + } +} + +function validation(): SchemaOf { + return object({ + certFile: withFileExtension(file(), ['pem', 'crt', 'cer', 'cert']).required( + '' + ), + keyFile: withFileExtension(file(), ['pem', 'key']).required(''), + forceHTTPS: bool().required(), + }); +} diff --git a/app/react/portainer/settings/queries/useSSLSettings.ts b/app/react/portainer/settings/queries/useSSLSettings.ts new file mode 100644 index 000000000..8567ba19a --- /dev/null +++ b/app/react/portainer/settings/queries/useSSLSettings.ts @@ -0,0 +1,24 @@ +import { useQuery } from 'react-query'; + +import axios, { parseAxiosError } from '@/portainer/services/axios'; + +interface SSLSettings { + certPath: string; + keyPath: string; + caCertPath: string; + selfSigned: boolean; + httpEnabled: boolean; +} + +export function useSSLSettings() { + return useQuery(['sslSettings'], async () => getSSLSettings()); +} + +async function getSSLSettings() { + try { + const response = await axios.get('/ssl'); + return response.data; + } catch (error) { + throw parseAxiosError(error, 'Unable to retrieve SSL settings'); + } +}