[WIFI-11273] Added captive to services configuration

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2022-10-31 09:35:44 +00:00
parent 980ad266ef
commit 46a3be7b2e
11 changed files with 365 additions and 4 deletions

View File

@@ -670,6 +670,7 @@
"version": "Ausführung"
},
"form": {
"captive_web_root_explanation": "Bitte verwenden Sie nur .tar-Dateien (keine komprimierten Dateien wie z. B. .targz)",
"certificate_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN CERTIFICATE-----“ beginnt und mit „-----END CERTIFICATE-----“ endet.",
"invalid_cidr": "Ungültige CIDR-IPv4-Adresse. Beispiel: 192.168.0.1/12",
"invalid_email": "Ungültige E-Mail",

View File

@@ -670,6 +670,7 @@
"version": "Version"
},
"form": {
"captive_web_root_explanation": "Please use .tar files only (no compressed files like .targz, for example)",
"certificate_file_explanation": "Please use a .pem file that starts with \"-----BEGIN CERTIFICATE-----\" and ends with \"-----END CERTIFICATE-----\"",
"invalid_cidr": "Invalid CIDR IPv4 address. Example: 192.168.0.1/12",
"invalid_email": "Invalid Email",

View File

@@ -670,6 +670,7 @@
"version": "Versión"
},
"form": {
"captive_web_root_explanation": "Utilice únicamente archivos .tar (no archivos comprimidos como .targz, por ejemplo)",
"certificate_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN CERTIFICATE-----\" y termine con \"-----END CERTIFICATE-----\"",
"invalid_cidr": "Dirección IPv4 CIDR no válida. Ejemplo: 192.168.0.1/12",
"invalid_email": "Email inválido",

View File

@@ -670,6 +670,7 @@
"version": "Version"
},
"form": {
"captive_web_root_explanation": "Veuillez utiliser uniquement des fichiers .tar (pas de fichiers compressés comme .targz, par exemple)",
"certificate_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN CERTIFICATE-----\" et se termine par \"-----END CERTIFICATE-----\"",
"invalid_cidr": "Adresse IPv4 CIDR non valide. Exemple : 192.168.0.1/12",
"invalid_email": "Email Invalide",

View File

@@ -670,6 +670,7 @@
"version": "Versão"
},
"form": {
"captive_web_root_explanation": "Por favor, use apenas arquivos .tar (sem arquivos compactados como .targz, por exemplo)",
"certificate_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN CERTIFICATE-----\" e termine com \"-----END CERTIFICATE-----\"",
"invalid_cidr": "Endereço CIDR IPv4 inválido. Exemplo: 192.168.0.1/12",
"invalid_email": "E-mail inválido",

View File

@@ -10,6 +10,7 @@ interface Props {
accept: string;
isHidden?: boolean;
isStringFile?: boolean;
wantBase64?: boolean;
}
const defaultProps = {
@@ -18,7 +19,16 @@ const defaultProps = {
isStringFile: false,
};
const FileInputButton = ({ value, setValue, setFileName, refreshId, accept, isHidden, isStringFile }: Props) => {
const FileInputButton = ({
value,
setValue,
setFileName,
refreshId,
accept,
isHidden,
isStringFile,
wantBase64,
}: Props) => {
const [fileKey, setFileKey] = useState(uuid());
let fileReader: FileReader | undefined;
@@ -31,11 +41,27 @@ const FileInputButton = ({ value, setValue, setFileName, refreshId, accept, isHi
}
};
const handleBase64FileRead = () => {
if (fileReader) {
const content = fileReader.result;
if (content && typeof content === 'string') {
const split = content.split('base64,');
if (split[1]) {
setValue(split[1] as string);
}
}
}
};
const changeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files ? e.target.files[0] : undefined;
if (file) {
const newVal = URL.createObjectURL(file);
if (!isStringFile) {
if (wantBase64) {
fileReader = new FileReader();
fileReader.onloadend = handleBase64FileRead;
fileReader.readAsDataURL(file);
} else if (!isStringFile) {
setValue(newVal, file);
if (setFileName) setFileName(file.name ?? '');
} else {

View File

@@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import ConfigurationFieldExplanation from '../ConfigurationFieldExplanation';
import DeleteButton from 'components/Buttons/DeleteButton';
import FileInputButton from 'components/Buttons/FileInputButton';
import SaveButton from 'components/Buttons/SaveButton';
import ModalHeader from 'components/Modals/ModalHeader';
@@ -39,6 +40,9 @@ const propTypes = {
error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
touched: PropTypes.bool,
definitionKey: PropTypes.string,
canDelete: PropTypes.bool.isRequired,
onDelete: PropTypes.func.isRequired,
wantBase64: PropTypes.bool,
};
const defaultProps = {
@@ -51,6 +55,7 @@ const defaultProps = {
error: false,
touched: false,
definitionKey: null,
wantBase64: false,
};
const FileInputModal = ({
@@ -67,6 +72,9 @@ const FileInputModal = ({
isDisabled,
isHidden,
definitionKey,
canDelete,
onDelete,
wantBase64,
}) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -108,9 +116,10 @@ const FileInputModal = ({
onClick={onOpen}
icon={<UploadSimple size={20} />}
isDisabled={isDisabled}
ml={2}
mx={2}
/>
</Tooltip>
{value !== undefined && canDelete && <DeleteButton onClick={onDelete} isCompact />}
</Text>
<FormErrorMessage>{error}</FormErrorMessage>
</FormControl>
@@ -135,7 +144,8 @@ const FileInputModal = ({
setFileName={setTempFilename}
refreshId={refreshId}
accept={acceptedFileTypes}
isStringFile
isStringFile={!wantBase64}
wantBase64={wantBase64}
/>
</Box>
<FormControl isInvalid={tempValue !== '' && !test(tempValue)}>

View File

@@ -14,6 +14,8 @@ const propTypes = {
isRequired: PropTypes.bool,
isHidden: PropTypes.bool,
definitionKey: PropTypes.string,
canDelete: PropTypes.bool,
wantBase64: PropTypes.bool,
};
const defaultProps = {
@@ -22,6 +24,8 @@ const defaultProps = {
isDisabled: false,
isHidden: false,
definitionKey: null,
canDelete: false,
wantBase64: false,
};
const FileInputFieldModal = ({
@@ -35,10 +39,16 @@ const FileInputFieldModal = ({
isRequired,
isHidden,
definitionKey,
canDelete,
wantBase64,
}) => {
const [{ value }, { touched, error }, { setValue }] = useField(name);
const [{ value: fileNameValue }, , { setValue: setFile }] = useField(fileName);
const onDelete = useCallback(() => {
setValue(undefined);
setFile(undefined);
}, []);
const onChange = useCallback((newValue, newFilename) => {
setValue(newValue);
setFile(newFilename);
@@ -59,6 +69,9 @@ const FileInputFieldModal = ({
isDisabled={isDisabled}
isHidden={isHidden}
definitionKey={definitionKey}
canDelete={canDelete}
onDelete={onDelete}
wantBase64={wantBase64}
/>
);
};

View File

@@ -0,0 +1,182 @@
import * as React from 'react';
import { Box, Heading, Select, SimpleGrid, Spacer, VStack } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { object, string } from 'yup';
import Card from 'components/Card';
import CardBody from 'components/Card/CardBody';
import CardHeader from 'components/Card/CardHeader';
import CreatableSelectField from 'components/FormFields/CreatableSelectField';
import FileInputFieldModal from 'components/FormFields/FileInputFieldModal';
import NumberField from 'components/FormFields/NumberField';
import ObjectArrayFieldModal from 'components/FormFields/ObjectArrayFieldModal';
import SelectField from 'components/FormFields/SelectField';
import StringField from 'components/FormFields/StringField';
import useFastField from 'hooks/useFastField';
const CREDENTIALS_SCHEMA = (t: (str: string) => string, useDefault = false) => {
const shape = object().shape({
username: string().required(t('form.required')).default(''),
password: string().required(t('form.required')).default(''),
});
return useDefault ? shape : shape.nullable().default(undefined);
};
const namePrefix = 'configuration.captive';
const CaptiveConfiguration = ({ editing }: { editing: boolean }) => {
const { t } = useTranslation();
const name = React.useCallback((suffix: string) => `${namePrefix}.${suffix}`, []);
const { value: captive, onChange } = useFastField({
name: namePrefix,
});
const handleAuthModeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
if (e.target.value === 'radius') {
onChange({
'walled-garden-fqdn': [],
'idle-timeout': 600,
'auth-mode': e.target.value,
'auth-server': '192.168.1.10',
'auth-secret': 'secret',
'aut-port': 1812,
});
} else if (e.target.value === 'uam') {
onChange({
'walled-garden-fqdn': [],
'idle-timeout': 600,
'auth-mode': e.target.value,
'auth-server': '192.168.1.10',
'auth-secret': 'secret',
'aut-port': 1812,
'uam-port': 3990,
'uam-secret': 'secret',
'uam-server': 'https://YOUR-LOGIN-ADDRESS.YOURS',
nasid: 'TestLab',
});
} else {
onChange({ 'walled-garden-fqdn': [], 'idle-timeout': 600, 'auth-mode': e.target.value });
}
};
const fieldProps = (suffix: string) => ({
name: name(suffix),
label: suffix,
definitionKey: `interface.ssid.pass-point.${suffix}`,
isDisabled: !editing,
});
const mode = captive?.['auth-mode'] as string | undefined;
const credFields = React.useMemo(
() => (
<SimpleGrid minChildWidth="300px" gap={4}>
<StringField name="username" label="username" isRequired />
<StringField name="password" label="password" isRequired />
</SimpleGrid>
),
[],
);
const credCols = React.useMemo(
() => [
{
id: 'username',
Header: 'username',
Footer: '',
accessor: 'username',
},
{
id: 'password',
Header: 'password',
Footer: '',
accessor: 'password',
},
],
[],
);
return (
<Card variant="widget" mb={4}>
<CardHeader>
<Heading my="auto" size="md" textDecor="underline">
Captive
</Heading>
<Spacer />
<Box>
<Select value={captive['auth-mode']} onChange={handleAuthModeChange} isDisabled={!editing}>
<option value="click">Click</option>
<option value="radius">Radius</option>
<option value="credentials">Credentials</option>
<option value="uam">UAM</option>
</Select>
</Box>
</CardHeader>
<CardBody pb={8} pt={2} display="block">
{
// Basic Fields
}
<VStack spacing={2}>
<CreatableSelectField {...fieldProps('walled-garden-fqdn')} placeholder="Example: *.google.com" isRequired />
<FileInputFieldModal
{...fieldProps('web-root')}
fileName="configuration.captive.web-root-filename"
definitionKey="service.captive.web-root"
explanation={t('form.captive_web_root_explanation')}
test={() => true}
acceptedFileTypes=".tar"
isDisabled={!editing}
canDelete
isRequired
wantBase64
/>
<NumberField {...fieldProps('idle-timeout')} isRequired w="100px" />
<NumberField {...fieldProps('session-timeout')} emptyIsUndefined acceptEmptyValue w="100px" />
{mode === 'credentials' && (
<ObjectArrayFieldModal
{...fieldProps('credentials')}
fields={credFields}
columns={credCols}
schema={CREDENTIALS_SCHEMA}
isDisabled={!editing}
isRequired
/>
)}
</VStack>
{mode === 'uam' && (
<VStack spacing={2}>
<StringField {...fieldProps('uam-server')} isRequired />
<StringField {...fieldProps('uam-secret')} isRequired hideButton />
<NumberField {...fieldProps('uam-port')} isRequired />
<StringField {...fieldProps('nasid')} isRequired />
<StringField {...fieldProps('nasmac')} />
<SelectField
{...fieldProps('mac-format')}
options={[
{ value: 'aabbccddeeff', label: 'aabbccddeeff' },
{ value: 'aa-bb-cc-dd-ee-ff', label: 'aa-bb-cc-dd-ee-ff' },
{ value: 'aa:bb:cc:dd:ee:ff', label: 'aa:bb:cc:dd:ee:ff' },
{ value: 'AABBCCDDEEFF', label: 'AABBCCDDEEFF' },
{ value: 'AA:BB:CC:DD:EE:FF', label: 'AA:BB:CC:DD:EE:FF' },
{ value: 'AA-BB-CC-DD-EE-FF', label: 'AA-BB-CC-DD-EE-FF' },
]}
isRequired
/>
<StringField {...fieldProps('ssid')} />
</VStack>
)}
{(mode === 'radius' || mode === 'uam') && (
<VStack spacing={2}>
<StringField {...fieldProps('auth-server')} isRequired />
<StringField {...fieldProps('auth-secret')} isRequired hideButton />
<NumberField {...fieldProps('auth-port')} isRequired />
<StringField {...fieldProps('acct-server')} emptyIsUndefined />
<StringField {...fieldProps('acct-secret')} emptyIsUndefined hideButton />
<NumberField {...fieldProps('acct-port')} emptyIsUndefined acceptEmptyValue />
<NumberField {...fieldProps('acct-interval')} emptyIsUndefined acceptEmptyValue />
</VStack>
)}
</CardBody>
</Card>
);
};
export default React.memo(CaptiveConfiguration);

View File

@@ -9,6 +9,7 @@ import InternalFormAccess from '../common/InternalFormAccess';
import SectionGeneralCard from '../common/SectionGeneralCard';
import SubSectionPicker from '../common/SubSectionPicker';
import AirtimePolicies from './AirtimePolicies';
import Captive from './Captive';
import DataPlane from './DataPlane';
import FacebookWifi from './FacebookWifi';
import Http from './Http';
@@ -121,6 +122,7 @@ const ServicesSection = ({ editing, setSection, sectionInformation, removeSub })
editing={editing}
subsections={[
'airtime-policies',
'captive',
'data-plane',
'facebook-wifi',
'http',
@@ -159,6 +161,7 @@ const ServicesSection = ({ editing, setSection, sectionInformation, removeSub })
{isSubSectionActive('data-plane') && <DataPlane editing={editing} />}
{isSubSectionActive('ieee8021x') && <Ieee8021x editing={editing} />}
{isSubSectionActive('radius-proxy') && <RadiusProxy editing={editing} />}
{isSubSectionActive('captive') && <Captive editing={editing} />}
</Masonry>
</>
)}

View File

@@ -1,6 +1,125 @@
import { object, number, string, array, bool } from 'yup';
import { testFqdnHostname, testIpv4, testLength, testUcMac } from 'constants/formTests';
export const SERVICES_CAPTIVE_SCHEMA = (t, useDefault = false) => {
const shape = object()
.shape({
'auto-mode': string().required(t('form.required')).default('click'),
'walled-garden-fqdn': array().of(string()).min(1, t('form.required')).default([]),
'web-root': string().default(undefined),
'idle-timeout': number().required(t('form.required')).positive().lessThan(65535).integer().default(600),
'session-timeout': number().positive().lessThan(65535).integer().default(undefined),
// Only if auto-mode is "credentials"
credentials: array()
.when('auth-mode', {
is: 'credentials',
then: array()
.of(
object().shape({
username: string().required(t('form.required')).default(''),
password: string().required(t('form.required')).default(''),
}),
)
.min(1, t('form.required')),
})
.default(undefined),
// Radius && UAM values
'auth-server': string()
.when('auth-mode', {
is: (authMode) => authMode === 'radius' || authMode === 'uam',
then: string().required(t('form.required')).default(''),
})
.default(undefined),
'auth-secret': string()
.when('auth-mode', {
is: (authMode) => authMode === 'radius' || authMode === 'uam',
then: string().required(t('form.required')).default(''),
else: string().default(undefined),
})
.default(undefined),
'auth-port': number()
.when('auth-mode', {
is: (authMode) => authMode === 'radius' || authMode === 'uam',
then: number().required(t('form.required')).moreThan(1023).lessThan(65535).integer().default(1812),
else: number().default(undefined),
})
.default(undefined),
'acct-server': string()
.when('auth-mode', {
is: (authMode) => authMode === 'radius' || authMode === 'uam',
then: string().default(undefined),
else: string().default(undefined),
})
.default(undefined),
'acct-secret': string()
.when('auth-mode', {
is: (authMode) => authMode === 'radius' || authMode === 'uam',
then: string().default(undefined),
else: string().default(undefined),
})
.default(undefined),
'acct-port': number()
.when('auth-mode', {
is: (authMode) => authMode === 'radius' || authMode === 'uam',
then: number().moreThan(1023).lessThan(65535).integer().default(undefined),
else: number().default(undefined),
})
.default(undefined),
'acct-interval': number()
.when('auth-mode', {
is: (authMode) => authMode === 'radius' || authMode === 'uam',
then: number().positive().lessThan(65535).integer().default(undefined),
})
.default(undefined),
// Only UAM fields
'uam-server': string()
.when('auth-mode', {
is: (authMode) => authMode === 'uam',
then: string().required(t('form.required')).default(''),
})
.default(undefined),
'uam-secret': string()
.when('auth-mode', {
is: (authMode) => authMode === 'uam',
then: string().required(t('form.required')).default(''),
})
.default(undefined),
'uam-port': number()
.when('auth-mode', {
is: (authMode) => authMode === 'uam',
then: number().required(t('form.required')).moreThan(1023).lessThan(65535).integer().default(3990),
})
.default(undefined),
ssid: string()
.when('auth-mode', {
is: (authMode) => authMode === 'uam',
then: string().default(undefined),
})
.default(undefined),
'mac-format': string()
.when('auth-mode', {
is: (authMode) => authMode === 'uam',
then: string().required(t('form.required')).default('aabbccddeeff'),
})
.default(undefined),
nasid: string()
.when('auth-mode', {
is: (authMode) => authMode === 'uam',
then: string().required(t('form.required')).default(''),
})
.default(undefined),
nasmac: string()
.when('auth-mode', {
is: (authMode) => authMode === 'uam',
then: string().default(undefined),
})
.default(undefined),
})
.default({});
return useDefault ? shape : shape.nullable().default(undefined);
};
export const SERVICES_CLASSIFIER_DNS_SCHEMA = (t, useDefault = false) => {
const shape = object().shape({
fqdn: string().default(''),
@@ -295,6 +414,7 @@ export const SERVICES_SCHEMA = (t, useDefault = false) =>
'data-plane': SERVICES_DATA_PLANE_SCHEMA(t, useDefault),
'radius-proxy': SERVICES_RADIUS_PROXY_SCHEMA(t, useDefault),
ieee8021x: SERVICES_IEEE8021X_SCHEMA(t, useDefault),
captive: SERVICES_CAPTIVE_SCHEMA(t, useDefault),
}),
});
@@ -334,6 +454,8 @@ export const getSubSectionDefaults = (t, sub) => {
return SERVICES_IEEE8021X_SCHEMA(t, true).cast();
case 'radius-proxy':
return SERVICES_RADIUS_PROXY_SCHEMA(t, true).cast();
case 'captive':
return SERVICES_CAPTIVE_SCHEMA(t, true).cast();
default:
return null;
}