mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
refactor: port auth admin to TS/SWR (#675)
* refactor: format files * refactor: add missing RE UI config flag * refactor: port admin auth index to TS/SWR * refactor: port GoogleAuth to TS/SWR * refactor: port OidcAuth to TS/SWR * refactor: port SamlAuth to TS/SWR * refactor: remove unused e-admin-auth store * refactor: make AutoCreateForm an explicit export * refactor: improve auth settings dir structure * refactor: destructure authenticationType from uiConfig * refactor: use setToastApiError to show errors * refactor: format files * refactor: remove invalid string fields from requests Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
234bab6cb4
commit
f4d5ed03aa
@ -1,16 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import AdminMenu from '../menu/AdminMenu';
|
import AdminMenu from '../menu/AdminMenu';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import GoogleAuth from './google-auth-container';
|
|
||||||
import SamlAuth from './saml-auth-container';
|
|
||||||
import OidcAuth from './oidc-auth-container';
|
|
||||||
import PasswordAuthSettings from './PasswordAuthSettings';
|
|
||||||
import TabNav from '../../common/TabNav/TabNav';
|
import TabNav from '../../common/TabNav/TabNav';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { OidcAuth } from './OidcAuth/OidcAuth';
|
||||||
|
import { SamlAuth } from './SamlAuth/SamlAuth';
|
||||||
|
import { PasswordAuth } from './PasswordAuth/PasswordAuth';
|
||||||
|
import { GoogleAuth } from './GoogleAuth/GoogleAuth';
|
||||||
|
|
||||||
|
export const AuthSettings = () => {
|
||||||
|
const { authenticationType } = useUiConfig().uiConfig;
|
||||||
|
|
||||||
function AdminAuthPage({ authenticationType, history }) {
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: 'OpenID Connect',
|
label: 'OpenID Connect',
|
||||||
@ -22,7 +24,7 @@ function AdminAuthPage({ authenticationType, history }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Password',
|
label: 'Password',
|
||||||
component: <PasswordAuthSettings />,
|
component: <PasswordAuth />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Google',
|
label: 'Google',
|
||||||
@ -32,7 +34,7 @@ function AdminAuthPage({ authenticationType, history }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<AdminMenu history={history} />
|
<AdminMenu />
|
||||||
<PageContent headerContent="Single Sign-On">
|
<PageContent headerContent="Single Sign-On">
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={authenticationType === 'enterprise'}
|
condition={authenticationType === 'enterprise'}
|
||||||
@ -80,12 +82,4 @@ function AdminAuthPage({ authenticationType, history }) {
|
|||||||
</PageContent>
|
</PageContent>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
AdminAuthPage.propTypes = {
|
|
||||||
match: PropTypes.object.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
authenticationType: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdminAuthPage;
|
|
@ -1,5 +1,14 @@
|
|||||||
import React, { ChangeEvent, Fragment } from 'react';
|
import React, { ChangeEvent, Fragment } from 'react';
|
||||||
import { FormControl, Grid, MenuItem, Switch, TextField, Select, InputLabel, FormControlLabel } from '@material-ui/core';
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
Grid,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
TextField,
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data?: {
|
data?: {
|
||||||
@ -7,25 +16,29 @@ interface Props {
|
|||||||
autoCreate: boolean;
|
autoCreate: boolean;
|
||||||
defaultRootRole?: string;
|
defaultRootRole?: string;
|
||||||
emailDomains?: string;
|
emailDomains?: string;
|
||||||
|
|
||||||
};
|
};
|
||||||
setValue: (name: string, value: string | boolean) => void;
|
setValue: (name: string, value: string | boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AutoCreateForm({ data = { enabled: false, autoCreate: false }, setValue }: Props) {
|
export const AutoCreateForm = ({
|
||||||
|
data = { enabled: false, autoCreate: false },
|
||||||
|
setValue,
|
||||||
|
}: Props) => {
|
||||||
const updateAutoCreate = () => {
|
const updateAutoCreate = () => {
|
||||||
setValue('autoCreate', !data.autoCreate);
|
setValue('autoCreate', !data.autoCreate);
|
||||||
}
|
};
|
||||||
|
|
||||||
const updateDefaultRootRole = (evt: ChangeEvent<{ name?: string; value: unknown }>) => {
|
const updateDefaultRootRole = (
|
||||||
|
evt: ChangeEvent<{ name?: string; value: unknown }>
|
||||||
|
) => {
|
||||||
setValue('defaultRootRole', evt.target.value as string);
|
setValue('defaultRootRole', evt.target.value as string);
|
||||||
}
|
};
|
||||||
|
|
||||||
const updateField = (e: ChangeEvent<HTMLInputElement>) => {
|
const updateField = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setValue(e.target.name, e.target.value);
|
setValue(e.target.name, e.target.value);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item md={5}>
|
<Grid item md={5}>
|
||||||
@ -35,14 +48,15 @@ return (
|
|||||||
</p>
|
</p>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={6} style={{ padding: '20px' }}>
|
<Grid item md={6} style={{ padding: '20px' }}>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={ <Switch
|
control={
|
||||||
|
<Switch
|
||||||
onChange={updateAutoCreate}
|
onChange={updateAutoCreate}
|
||||||
name="enabled"
|
name="enabled"
|
||||||
checked={data.autoCreate}
|
checked={data.autoCreate}
|
||||||
disabled={!data.enabled}
|
disabled={!data.enabled}
|
||||||
/>}
|
/>
|
||||||
|
}
|
||||||
label="Auto-create users"
|
label="Auto-create users"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -51,12 +65,15 @@ return (
|
|||||||
<Grid item md={5}>
|
<Grid item md={5}>
|
||||||
<strong>Default Root Role</strong>
|
<strong>Default Root Role</strong>
|
||||||
<p>
|
<p>
|
||||||
Choose which root role the user should get when no explicit role mapping exists.
|
Choose which root role the user should get when no
|
||||||
|
explicit role mapping exists.
|
||||||
</p>
|
</p>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={6}>
|
<Grid item md={6}>
|
||||||
<FormControl style={{minWidth: '200px'}}>
|
<FormControl style={{ minWidth: '200px' }}>
|
||||||
<InputLabel id="defaultRootRole-label">Default Role</InputLabel>
|
<InputLabel id="defaultRootRole-label">
|
||||||
|
Default Role
|
||||||
|
</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="defaultRootRole-label"
|
labelId="defaultRootRole-label"
|
||||||
id="defaultRootRole"
|
id="defaultRootRole"
|
||||||
@ -66,9 +83,9 @@ return (
|
|||||||
onChange={updateDefaultRootRole}
|
onChange={updateDefaultRootRole}
|
||||||
>
|
>
|
||||||
{/*consider these from API or constants. */}
|
{/*consider these from API or constants. */}
|
||||||
<MenuItem value='Viewer'>Viewer</MenuItem>
|
<MenuItem value="Viewer">Viewer</MenuItem>
|
||||||
<MenuItem value='Editor'>Editor</MenuItem>
|
<MenuItem value="Editor">Editor</MenuItem>
|
||||||
<MenuItem value='Admin'>Admin</MenuItem>
|
<MenuItem value="Admin">Admin</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -77,8 +94,8 @@ return (
|
|||||||
<Grid item md={5}>
|
<Grid item md={5}>
|
||||||
<strong>Email domains</strong>
|
<strong>Email domains</strong>
|
||||||
<p>
|
<p>
|
||||||
Comma separated list of email domains
|
Comma separated list of email domains that should be
|
||||||
that should be allowed to sign in.
|
allowed to sign in.
|
||||||
</p>
|
</p>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={6}>
|
<Grid item md={6}>
|
||||||
@ -97,6 +114,6 @@ return (
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Fragment>);
|
</Fragment>
|
||||||
}
|
);
|
||||||
export default AutoCreateForm;
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
@ -8,30 +7,32 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../../common/PageContent/PageContent';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../../contexts/AccessContext';
|
||||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||||
|
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
|
||||||
|
import useAuthSettingsApi from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
|
||||||
|
import useToast from '../../../../hooks/useToast';
|
||||||
|
import { formatUnknownError } from '../../../../utils/format-unknown-error';
|
||||||
|
import { removeEmptyStringFields } from '../../../../utils/remove-empty-string-fields';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
autoCreate: false,
|
autoCreate: false,
|
||||||
unleashHostname: location.hostname,
|
unleashHostname: location.hostname,
|
||||||
|
clientId: '',
|
||||||
|
clientSecret: '',
|
||||||
|
emailDomains: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
function GoogleAuth({
|
export const GoogleAuth = () => {
|
||||||
config,
|
const { setToastData, setToastApiError } = useToast();
|
||||||
getGoogleConfig,
|
const { uiConfig } = useUiConfig();
|
||||||
updateGoogleConfig,
|
|
||||||
unleashUrl,
|
|
||||||
}) {
|
|
||||||
const [data, setData] = useState(initialState);
|
const [data, setData] = useState(initialState);
|
||||||
const [info, setInfo] = useState();
|
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
const { config } = useAuthSettings('google');
|
||||||
useEffect(() => {
|
const { updateSettings, errors, loading } = useAuthSettingsApi('google');
|
||||||
getGoogleConfig();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config.clientId) {
|
if (config.clientId) {
|
||||||
@ -43,10 +44,10 @@ function GoogleAuth({
|
|||||||
return <span>You need admin privileges to access this section.</span>;
|
return <span>You need admin privileges to access this section.</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateField = e => {
|
const updateField = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setData({
|
setData({
|
||||||
...data,
|
...data,
|
||||||
[e.target.name]: e.target.value,
|
[event.target.name]: event.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,19 +59,22 @@ function GoogleAuth({
|
|||||||
setData({ ...data, autoCreate: !data.autoCreate });
|
setData({ ...data, autoCreate: !data.autoCreate });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async e => {
|
const onSubmit = async (event: React.SyntheticEvent) => {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
setInfo('...saving');
|
|
||||||
try {
|
try {
|
||||||
await updateGoogleConfig(data);
|
await updateSettings(removeEmptyStringFields(data));
|
||||||
setInfo('Settings stored');
|
setToastData({
|
||||||
setTimeout(() => setInfo(''), 2000);
|
title: 'Settings stored',
|
||||||
} catch (e) {
|
type: 'success',
|
||||||
setInfo(e.message);
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setToastApiError(formatUnknownError(err));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent>
|
<PageContent headerContent="">
|
||||||
<Grid container style={{ marginBottom: '1rem' }}>
|
<Grid container style={{ marginBottom: '1rem' }}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
@ -84,7 +88,7 @@ function GoogleAuth({
|
|||||||
</a>{' '}
|
</a>{' '}
|
||||||
to learn how to integrate with Google OAuth 2.0. <br />
|
to learn how to integrate with Google OAuth 2.0. <br />
|
||||||
Callback URL:{' '}
|
Callback URL:{' '}
|
||||||
<code>{unleashUrl}/auth/google/callback</code>
|
<code>{uiConfig.unleashUrl}/auth/google/callback</code>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -125,7 +129,7 @@ function GoogleAuth({
|
|||||||
label="Client ID"
|
label="Client ID"
|
||||||
name="clientId"
|
name="clientId"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
value={data.clientId || ''}
|
value={data.clientId}
|
||||||
style={{ width: '400px' }}
|
style={{ width: '400px' }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
@ -146,7 +150,7 @@ function GoogleAuth({
|
|||||||
onChange={updateField}
|
onChange={updateField}
|
||||||
label="Client Secret"
|
label="Client Secret"
|
||||||
name="clientSecret"
|
name="clientSecret"
|
||||||
value={data.clientSecret || ''}
|
value={data.clientSecret}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
style={{ width: '400px' }}
|
style={{ width: '400px' }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -195,9 +199,7 @@ function GoogleAuth({
|
|||||||
onChange={updateAutoCreate}
|
onChange={updateAutoCreate}
|
||||||
name="enabled"
|
name="enabled"
|
||||||
checked={data.autoCreate}
|
checked={data.autoCreate}
|
||||||
>
|
/>
|
||||||
Auto-create users
|
|
||||||
</Switch>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
@ -229,22 +231,18 @@ function GoogleAuth({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>{' '}
|
</Button>{' '}
|
||||||
<small>{info}</small>
|
<p>
|
||||||
|
<small style={{ color: 'red' }}>
|
||||||
|
{errors?.message}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</form>
|
</form>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
GoogleAuth.propTypes = {
|
|
||||||
config: PropTypes.object,
|
|
||||||
unleashUrl: PropTypes.string,
|
|
||||||
getGoogleConfig: PropTypes.func.isRequired,
|
|
||||||
updateGoogleConfig: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GoogleAuth;
|
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
@ -8,34 +7,40 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../../common/PageContent/PageContent';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../../contexts/AccessContext';
|
||||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||||
import AutoCreateForm from './AutoCreateForm/AutoCreateForm';
|
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
|
||||||
|
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import useAuthSettingsApi from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
|
||||||
|
import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
|
||||||
|
import useToast from '../../../../hooks/useToast';
|
||||||
|
import { formatUnknownError } from '../../../../utils/format-unknown-error';
|
||||||
|
import { removeEmptyStringFields } from '../../../../utils/remove-empty-string-fields';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
enableSingleSignOut: false,
|
enableSingleSignOut: false,
|
||||||
autoCreate: false,
|
autoCreate: false,
|
||||||
unleashHostname: location.hostname,
|
unleashHostname: location.hostname,
|
||||||
|
clientId: '',
|
||||||
|
discoverUrl: '',
|
||||||
|
secret: '',
|
||||||
|
acrValues: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
|
export const OidcAuth = () => {
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
const [data, setData] = useState(initialState);
|
const [data, setData] = useState(initialState);
|
||||||
const [info, setInfo] = useState();
|
|
||||||
const [error, setError] = useState();
|
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
const { config } = useAuthSettings('oidc');
|
||||||
useEffect(() => {
|
const { updateSettings, errors, loading } = useAuthSettingsApi('oidc');
|
||||||
getOidcConfig();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config.discoverUrl) {
|
if (config.discoverUrl) {
|
||||||
setData(config);
|
setData(config);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
if (!hasAccess(ADMIN)) {
|
if (!hasAccess(ADMIN)) {
|
||||||
@ -46,8 +51,8 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateField = e => {
|
const updateField = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setValue(e.target.name, e.target.value);
|
setValue(event.target.name, event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateEnabled = () => {
|
const updateEnabled = () => {
|
||||||
@ -58,28 +63,29 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
|
|||||||
setData({ ...data, enableSingleSignOut: !data.enableSingleSignOut });
|
setData({ ...data, enableSingleSignOut: !data.enableSingleSignOut });
|
||||||
};
|
};
|
||||||
|
|
||||||
const setValue = (field, value) => {
|
const setValue = (name: string, value: string | boolean) => {
|
||||||
setData({
|
setData({
|
||||||
...data,
|
...data,
|
||||||
[field]: value,
|
[name]: value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async e => {
|
const onSubmit = async (event: React.SyntheticEvent) => {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
setInfo('...saving');
|
|
||||||
setError('');
|
|
||||||
try {
|
try {
|
||||||
await updateOidcConfig(data);
|
await updateSettings(removeEmptyStringFields(data));
|
||||||
setInfo('Settings stored');
|
setToastData({
|
||||||
setTimeout(() => setInfo(''), 2000);
|
title: 'Settings stored',
|
||||||
} catch (e) {
|
type: 'success',
|
||||||
setInfo('');
|
});
|
||||||
setError(e.message);
|
} catch (err) {
|
||||||
|
setToastApiError(formatUnknownError(err));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent>
|
<PageContent headerContent="">
|
||||||
<Grid container style={{ marginBottom: '1rem' }}>
|
<Grid container style={{ marginBottom: '1rem' }}>
|
||||||
<Grid item md={12}>
|
<Grid item md={12}>
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
@ -94,7 +100,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
|
|||||||
to learn how to integrate with specific Open Id Connect
|
to learn how to integrate with specific Open Id Connect
|
||||||
providers (Okta, Keycloak, Google, etc). <br />
|
providers (Okta, Keycloak, Google, etc). <br />
|
||||||
Callback URL:{' '}
|
Callback URL:{' '}
|
||||||
<code>{unleashUrl}/auth/oidc/callback</code>
|
<code>{uiConfig.unleashUrl}/auth/oidc/callback</code>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -128,7 +134,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
|
|||||||
onChange={updateField}
|
onChange={updateField}
|
||||||
label="Discover URL"
|
label="Discover URL"
|
||||||
name="discoverUrl"
|
name="discoverUrl"
|
||||||
value={data.discoverUrl || ''}
|
value={data.discoverUrl}
|
||||||
disabled={!data.enabled}
|
disabled={!data.enabled}
|
||||||
style={{ width: '400px' }}
|
style={{ width: '400px' }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -146,7 +152,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
|
|||||||
onChange={updateField}
|
onChange={updateField}
|
||||||
label="Client ID"
|
label="Client ID"
|
||||||
name="clientId"
|
name="clientId"
|
||||||
value={data.clientId || ''}
|
value={data.clientId}
|
||||||
disabled={!data.enabled}
|
disabled={!data.enabled}
|
||||||
style={{ width: '400px' }}
|
style={{ width: '400px' }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -167,7 +173,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
|
|||||||
onChange={updateField}
|
onChange={updateField}
|
||||||
label="Client Secret"
|
label="Client Secret"
|
||||||
name="secret"
|
name="secret"
|
||||||
value={data.secret || ''}
|
value={data.secret}
|
||||||
disabled={!data.enabled}
|
disabled={!data.enabled}
|
||||||
style={{ width: '400px' }}
|
style={{ width: '400px' }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -180,7 +186,10 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
|
|||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item md={5}>
|
<Grid item md={5}>
|
||||||
<strong>Enable Single Sign-Out</strong>
|
<strong>Enable Single Sign-Out</strong>
|
||||||
<p>If you enable Single Sign-Out Unleash will redirect the user to the IDP as part of the Sign-out process.</p>
|
<p>
|
||||||
|
If you enable Single Sign-Out Unleash will redirect
|
||||||
|
the user to the IDP as part of the Sign-out process.
|
||||||
|
</p>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={6} style={{ padding: '20px' }}>
|
<Grid item md={6} style={{ padding: '20px' }}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
@ -204,15 +213,21 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
|
|||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item md={5}>
|
<Grid item md={5}>
|
||||||
<strong>ACR Values</strong>
|
<strong>ACR Values</strong>
|
||||||
<p>Requested Authentication Context Class Reference values. If multiple values are specified they should be "space" separated. Will be sent as "acr_values" as
|
<p>
|
||||||
part of the authentication request. Unleash will validate the acr value in the id token claims against the list of acr values.</p>
|
Requested Authentication Context Class Reference
|
||||||
|
values. If multiple values are specified they should
|
||||||
|
be "space" separated. Will be sent as "acr_values"
|
||||||
|
as part of the authentication request. Unleash will
|
||||||
|
validate the acr value in the id token claims
|
||||||
|
against the list of acr values.
|
||||||
|
</p>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={6}>
|
<Grid item md={6}>
|
||||||
<TextField
|
<TextField
|
||||||
onChange={updateField}
|
onChange={updateField}
|
||||||
label="ACR Values"
|
label="ACR Values"
|
||||||
name="acrValues"
|
name="acrValues"
|
||||||
value={data.acrValues || ''}
|
value={data.acrValues}
|
||||||
disabled={!data.enabled}
|
disabled={!data.enabled}
|
||||||
style={{ width: '400px' }}
|
style={{ width: '400px' }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -229,23 +244,18 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>{' '}
|
</Button>{' '}
|
||||||
<small>{info}</small>
|
<p>
|
||||||
<small style={{ color: 'red' }}>{error}</small>
|
<small style={{ color: 'red' }}>
|
||||||
|
{errors?.message}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</form>
|
</form>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
OidcAuth.propTypes = {
|
|
||||||
config: PropTypes.object,
|
|
||||||
unleash: PropTypes.string,
|
|
||||||
getOidcConfig: PropTypes.func.isRequired,
|
|
||||||
updateOidcConfig: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OidcAuth;
|
|
@ -1,30 +1,28 @@
|
|||||||
import React, { useState, useContext, useEffect } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import {
|
import { Button, FormControlLabel, Grid, Switch } from '@material-ui/core';
|
||||||
Button,
|
|
||||||
FormControlLabel,
|
|
||||||
Grid,
|
|
||||||
Switch,
|
|
||||||
} from '@material-ui/core';
|
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../../common/PageContent/PageContent';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../../contexts/AccessContext';
|
||||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||||
import useAuthSettings from '../../../hooks/api/getters/useAuthSettings/useAuthSettings';
|
import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
|
||||||
import useAuthSettingsApi, {ISimpleAuthSettings } from '../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
|
import useAuthSettingsApi, {
|
||||||
import useToast from '../../../hooks/useToast';
|
ISimpleAuthSettings,
|
||||||
|
} from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
|
||||||
|
import useToast from '../../../../hooks/useToast';
|
||||||
|
import { formatUnknownError } from '../../../../utils/format-unknown-error';
|
||||||
|
|
||||||
const PasswordAuthSettings = () => {
|
export const PasswordAuth = () => {
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { setToastData } = useToast();
|
|
||||||
const { config } = useAuthSettings('simple');
|
const { config } = useAuthSettings('simple');
|
||||||
const [disablePasswordAuth, setDisablePasswordAuth] = useState<boolean>(false);
|
const [disablePasswordAuth, setDisablePasswordAuth] =
|
||||||
const { updateSettings, errors, loading } = useAuthSettingsApi<ISimpleAuthSettings>('simple')
|
useState<boolean>(false);
|
||||||
|
const { updateSettings, errors, loading } =
|
||||||
|
useAuthSettingsApi<ISimpleAuthSettings>('simple');
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDisablePasswordAuth(!!config.disabled);
|
setDisablePasswordAuth(!!config.disabled);
|
||||||
}, [ config.disabled ]);
|
}, [config.disabled]);
|
||||||
|
|
||||||
if (!hasAccess(ADMIN)) {
|
if (!hasAccess(ADMIN)) {
|
||||||
return (
|
return (
|
||||||
@ -38,12 +36,13 @@ const PasswordAuthSettings = () => {
|
|||||||
setDisablePasswordAuth(!disablePasswordAuth);
|
setDisablePasswordAuth(!disablePasswordAuth);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (event: React.SyntheticEvent) => {
|
||||||
const onSubmit = async evt => {
|
event.preventDefault();
|
||||||
evt.preventDefault();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings: ISimpleAuthSettings = { disabled: disablePasswordAuth };
|
const settings: ISimpleAuthSettings = {
|
||||||
|
disabled: disablePasswordAuth,
|
||||||
|
};
|
||||||
await updateSettings(settings);
|
await updateSettings(settings);
|
||||||
setToastData({
|
setToastData({
|
||||||
title: 'Successfully saved',
|
title: 'Successfully saved',
|
||||||
@ -52,20 +51,13 @@ const PasswordAuthSettings = () => {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
show: true,
|
show: true,
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
setToastData({
|
setToastApiError(formatUnknownError(err));
|
||||||
title: 'Could not store settings',
|
setDisablePasswordAuth(config.disabled);
|
||||||
text: err?.message,
|
|
||||||
autoHideDuration: 4000,
|
|
||||||
type: 'error',
|
|
||||||
show: true,
|
|
||||||
});
|
|
||||||
setDisablePasswordAuth(config.disabled)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<PageContent headerContent=''>
|
<PageContent headerContent="">
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item md={5}>
|
<Grid item md={5}>
|
||||||
@ -82,7 +74,9 @@ const PasswordAuthSettings = () => {
|
|||||||
checked={!disablePasswordAuth}
|
checked={!disablePasswordAuth}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={!disablePasswordAuth ? 'Enabled' : 'Disabled'}
|
label={
|
||||||
|
!disablePasswordAuth ? 'Enabled' : 'Disabled'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -96,12 +90,14 @@ const PasswordAuthSettings = () => {
|
|||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>{' '}
|
</Button>{' '}
|
||||||
<p><small style={{ color: 'red' }}>{errors?.message}</small></p>
|
<p>
|
||||||
|
<small style={{ color: 'red' }}>
|
||||||
|
{errors?.message}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</form>
|
</form>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PasswordAuthSettings;
|
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
@ -8,32 +7,40 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../../common/PageContent/PageContent';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../../contexts/AccessContext';
|
||||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||||
import AutoCreateForm from './AutoCreateForm/AutoCreateForm';
|
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
|
||||||
|
import useToast from '../../../../hooks/useToast';
|
||||||
|
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
|
||||||
|
import useAuthSettingsApi from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
|
||||||
|
import { formatUnknownError } from '../../../../utils/format-unknown-error';
|
||||||
|
import { removeEmptyStringFields } from '../../../../utils/remove-empty-string-fields';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
autoCreate: false,
|
autoCreate: false,
|
||||||
unleashHostname: location.hostname,
|
unleashHostname: location.hostname,
|
||||||
|
entityId: '',
|
||||||
|
signOnUrl: '',
|
||||||
|
certificate: '',
|
||||||
|
signOutUrl: '',
|
||||||
|
spCertificate: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
export const SamlAuth = () => {
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
const [data, setData] = useState(initialState);
|
const [data, setData] = useState(initialState);
|
||||||
const [info, setInfo] = useState();
|
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
const { config } = useAuthSettings('saml');
|
||||||
useEffect(() => {
|
const { updateSettings, errors, loading } = useAuthSettingsApi('saml');
|
||||||
getSamlConfig();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config.entityId) {
|
if (config.entityId) {
|
||||||
setData(config);
|
setData(config);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
if (!hasAccess(ADMIN)) {
|
if (!hasAccess(ADMIN)) {
|
||||||
@ -44,34 +51,37 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateField = e => {
|
const updateField = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setValue(e.target.name, e.target.value);
|
setValue(event.target.name, event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateEnabled = () => {
|
const updateEnabled = () => {
|
||||||
setData({ ...data, enabled: !data.enabled });
|
setData({ ...data, enabled: !data.enabled });
|
||||||
};
|
};
|
||||||
|
|
||||||
const setValue = (field, value) => {
|
const setValue = (name: string, value: string | boolean) => {
|
||||||
setData({
|
setData({
|
||||||
...data,
|
...data,
|
||||||
[field]: value,
|
[name]: value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async e => {
|
const onSubmit = async (event: React.SyntheticEvent) => {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
setInfo('...saving');
|
|
||||||
try {
|
try {
|
||||||
await updateSamlConfig(data);
|
await updateSettings(removeEmptyStringFields(data));
|
||||||
setInfo('Settings stored');
|
setToastData({
|
||||||
setTimeout(() => setInfo(''), 2000);
|
title: 'Settings stored',
|
||||||
} catch (e) {
|
type: 'success',
|
||||||
setInfo(e.message);
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setToastApiError(formatUnknownError(err));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent>
|
<PageContent headerContent="">
|
||||||
<Grid container style={{ marginBottom: '1rem' }}>
|
<Grid container style={{ marginBottom: '1rem' }}>
|
||||||
<Grid item md={12}>
|
<Grid item md={12}>
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
@ -86,7 +96,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
|||||||
to learn how to integrate with specific SAML 2.0
|
to learn how to integrate with specific SAML 2.0
|
||||||
providers (Okta, Keycloak, etc). <br />
|
providers (Okta, Keycloak, etc). <br />
|
||||||
Callback URL:{' '}
|
Callback URL:{' '}
|
||||||
<code>{unleashUrl}/auth/saml/callback</code>
|
<code>{uiConfig.unleashUrl}/auth/saml/callback</code>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -120,7 +130,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
|||||||
onChange={updateField}
|
onChange={updateField}
|
||||||
label="Entity ID"
|
label="Entity ID"
|
||||||
name="entityId"
|
name="entityId"
|
||||||
value={data.entityId || ''}
|
value={data.entityId}
|
||||||
disabled={!data.enabled}
|
disabled={!data.enabled}
|
||||||
style={{ width: '400px' }}
|
style={{ width: '400px' }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -142,7 +152,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
|||||||
onChange={updateField}
|
onChange={updateField}
|
||||||
label="Single Sign-On URL"
|
label="Single Sign-On URL"
|
||||||
name="signOnUrl"
|
name="signOnUrl"
|
||||||
value={data.signOnUrl || ''}
|
value={data.signOnUrl}
|
||||||
disabled={!data.enabled}
|
disabled={!data.enabled}
|
||||||
style={{ width: '400px' }}
|
style={{ width: '400px' }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -164,7 +174,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
|||||||
onChange={updateField}
|
onChange={updateField}
|
||||||
label="X.509 Certificate"
|
label="X.509 Certificate"
|
||||||
name="certificate"
|
name="certificate"
|
||||||
value={data.certificate || ''}
|
value={data.certificate}
|
||||||
disabled={!data.enabled}
|
disabled={!data.enabled}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -196,7 +206,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
|||||||
onChange={updateField}
|
onChange={updateField}
|
||||||
label="Single Sign-out URL"
|
label="Single Sign-out URL"
|
||||||
name="signOutUrl"
|
name="signOutUrl"
|
||||||
value={data.signOutUrl || ''}
|
value={data.signOutUrl}
|
||||||
disabled={!data.enabled}
|
disabled={!data.enabled}
|
||||||
style={{ width: '400px' }}
|
style={{ width: '400px' }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -219,7 +229,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
|||||||
onChange={updateField}
|
onChange={updateField}
|
||||||
label="X.509 Certificate"
|
label="X.509 Certificate"
|
||||||
name="spCertificate"
|
name="spCertificate"
|
||||||
value={data.spCertificate || ''}
|
value={data.spCertificate}
|
||||||
disabled={!data.enabled}
|
disabled={!data.enabled}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -243,22 +253,18 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>{' '}
|
</Button>{' '}
|
||||||
<small>{info}</small>
|
<p>
|
||||||
|
<small style={{ color: 'red' }}>
|
||||||
|
{errors?.message}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</form>
|
</form>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
SamlAuth.propTypes = {
|
|
||||||
config: PropTypes.object,
|
|
||||||
unleash: PropTypes.string,
|
|
||||||
getSamlConfig: PropTypes.func.isRequired,
|
|
||||||
updateSamlConfig: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SamlAuth;
|
|
@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import GoogleAuth from './google-auth';
|
|
||||||
import { getGoogleConfig, updateGoogleConfig } from '../../../store/e-admin-auth/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
config: state.authAdmin.get('google'),
|
|
||||||
unleashUrl: state.uiConfig.toJS().unleashUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Container = connect(mapStateToProps, { getGoogleConfig, updateGoogleConfig })(GoogleAuth);
|
|
||||||
|
|
||||||
export default Container;
|
|
@ -1,10 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import component from './authentication';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
authenticationType: state.uiConfig.toJS().authenticationType,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Container = connect(mapStateToProps, { })(component);
|
|
||||||
|
|
||||||
export default Container;
|
|
@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import OidcAuth from './oidc-auth';
|
|
||||||
import { getOidcConfig, updateOidcConfig } from '../../../store/e-admin-auth/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
config: state.authAdmin.get('oidc'),
|
|
||||||
unleashUrl: state.uiConfig.toJS().unleashUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
const OidcContainer = connect(mapStateToProps, { getOidcConfig, updateOidcConfig })(OidcAuth);
|
|
||||||
|
|
||||||
export default OidcContainer;
|
|
@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import SamlAuth from './saml-auth';
|
|
||||||
import { getSamlConfig, updateSamlConfig } from '../../../store/e-admin-auth/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
config: state.authAdmin.get('saml'),
|
|
||||||
unleashUrl: state.uiConfig.toJS().unleashUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Container = connect(mapStateToProps, { getSamlConfig, updateSamlConfig })(SamlAuth);
|
|
||||||
|
|
||||||
export default Container;
|
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
import { Paper, Tabs, Tab } from '@material-ui/core';
|
import { Paper, Tab, Tabs } from '@material-ui/core';
|
||||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const navLinkStyle = {
|
const navLinkStyle = {
|
||||||
@ -13,18 +13,17 @@ const navLinkStyle = {
|
|||||||
padding: '0.8rem 1.5rem',
|
padding: '0.8rem 1.5rem',
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeNavLinkStyle = {
|
const activeNavLinkStyle: React.CSSProperties = {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
padding: '0.8rem 1.5rem',
|
padding: '0.8rem 1.5rem',
|
||||||
};
|
};
|
||||||
|
|
||||||
function AdminMenu({ history }) {
|
function AdminMenu() {
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
|
const { pathname } = useLocation();
|
||||||
const { flags } = uiConfig;
|
const { flags } = uiConfig;
|
||||||
|
|
||||||
const { location } = history;
|
|
||||||
const { pathname } = location;
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
style={{
|
style={{
|
||||||
@ -45,7 +44,7 @@ function AdminMenu({ history }) {
|
|||||||
<span>Users</span>
|
<span>Users</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
}
|
}
|
||||||
></Tab>
|
/>
|
||||||
{flags.RE && (
|
{flags.RE && (
|
||||||
<Tab
|
<Tab
|
||||||
value="/admin/roles"
|
value="/admin/roles"
|
||||||
@ -58,7 +57,7 @@ function AdminMenu({ history }) {
|
|||||||
<span>PROJECT ROLES</span>
|
<span>PROJECT ROLES</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
}
|
}
|
||||||
></Tab>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
@ -72,7 +71,7 @@ function AdminMenu({ history }) {
|
|||||||
API Access
|
API Access
|
||||||
</NavLink>
|
</NavLink>
|
||||||
}
|
}
|
||||||
></Tab>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
value="/admin/auth"
|
value="/admin/auth"
|
||||||
label={
|
label={
|
||||||
@ -84,7 +83,7 @@ function AdminMenu({ history }) {
|
|||||||
Single Sign-On
|
Single Sign-On
|
||||||
</NavLink>
|
</NavLink>
|
||||||
}
|
}
|
||||||
></Tab>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@ -61,6 +61,7 @@ const TabNav = ({ tabData, className, navClass, startingTab = 0 }) => {
|
|||||||
|
|
||||||
TabNav.propTypes = {
|
TabNav.propTypes = {
|
||||||
tabData: PropTypes.array.isRequired,
|
tabData: PropTypes.array.isRequired,
|
||||||
|
navClass: PropTypes.string,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
startingTab: PropTypes.number,
|
startingTab: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
@ -398,12 +398,7 @@ Array [
|
|||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": Object {
|
"component": [Function],
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
|
||||||
"type": [Function],
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"menu": Object {
|
"menu": Object {
|
||||||
"adminSettings": true,
|
"adminSettings": true,
|
||||||
|
@ -16,7 +16,7 @@ import Admin from '../admin';
|
|||||||
import AdminApi from '../admin/api';
|
import AdminApi from '../admin/api';
|
||||||
import AdminInvoice from '../admin/invoice/InvoiceAdminPage';
|
import AdminInvoice from '../admin/invoice/InvoiceAdminPage';
|
||||||
import AdminUsers from '../admin/users/UsersAdmin';
|
import AdminUsers from '../admin/users/UsersAdmin';
|
||||||
import AdminAuth from '../admin/auth';
|
import { AuthSettings } from '../admin/auth/AuthSettings';
|
||||||
import Login from '../user/Login/Login';
|
import Login from '../user/Login/Login';
|
||||||
import { P, C, E, EEA, RE } from '../common/flags';
|
import { P, C, E, EEA, RE } from '../common/flags';
|
||||||
import NewUser from '../user/NewUser';
|
import NewUser from '../user/NewUser';
|
||||||
@ -446,7 +446,7 @@ export const routes = [
|
|||||||
path: '/admin/auth',
|
path: '/admin/auth',
|
||||||
parent: '/admin',
|
parent: '/admin',
|
||||||
title: 'Single Sign-On',
|
title: 'Single Sign-On',
|
||||||
component: AdminAuth,
|
component: AuthSettings,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
|
@ -46,7 +46,7 @@ const useAPI = ({
|
|||||||
handleUnauthorized,
|
handleUnauthorized,
|
||||||
propagateErrors = false,
|
propagateErrors = false,
|
||||||
}: IUseAPI) => {
|
}: IUseAPI) => {
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const defaultOptions: RequestInit = {
|
const defaultOptions: RequestInit = {
|
||||||
|
@ -12,7 +12,6 @@ export const handleBadRequest = async (
|
|||||||
if (!setErrors) return;
|
if (!setErrors) return;
|
||||||
if (res) {
|
if (res) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
setErrors({message: data.message});
|
setErrors({message: data.message});
|
||||||
throw new Error(data.message);
|
throw new Error(data.message);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ export const defaultValue = {
|
|||||||
version: '3.x',
|
version: '3.x',
|
||||||
environment: '',
|
environment: '',
|
||||||
slogan: 'The enterprise ready feature toggle service.',
|
slogan: 'The enterprise ready feature toggle service.',
|
||||||
flags: { P: false, C: false, E: false },
|
flags: { P: false, C: false, E: false, RE: false },
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
value: 'Documentation',
|
value: 'Documentation',
|
||||||
|
@ -15,6 +15,7 @@ export interface IFlags {
|
|||||||
C: boolean;
|
C: boolean;
|
||||||
P: boolean;
|
P: boolean;
|
||||||
E: boolean;
|
E: boolean;
|
||||||
|
RE: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
import api from './api';
|
|
||||||
import { dispatchError } from '../util';
|
|
||||||
export const RECIEVE_GOOGLE_CONFIG = 'RECIEVE_GOOGLE_CONFIG';
|
|
||||||
export const RECIEVE_GOOGLE_CONFIG_ERROR = 'RECIEVE_GOOGLE_CONFIG_ERROR';
|
|
||||||
export const UPDATE_GOOGLE_AUTH = 'UPDATE_GOOGLE_AUTH';
|
|
||||||
export const UPDATE_GOOGLE_AUTH_ERROR = 'UPDATE_GOOGLE_AUTH_ERROR';
|
|
||||||
export const RECIEVE_SAML_CONFIG = 'RECIEVE_SAML_CONFIG';
|
|
||||||
export const RECIEVE_SAML_CONFIG_ERROR = 'RECIEVE_SAML_CONFIG_ERROR';
|
|
||||||
export const UPDATE_SAML_AUTH = 'UPDATE_SAML_AUTH';
|
|
||||||
export const UPDATE_SAML_AUTH_ERROR = 'UPDATE_SAML_AUTH_ERROR';
|
|
||||||
export const RECIEVE_OIDC_CONFIG = 'RECIEVE_OIDC_CONFIG';
|
|
||||||
export const RECIEVE_OIDC_CONFIG_ERROR = 'RECIEVE_OIDC_CONFIG_ERROR';
|
|
||||||
export const UPDATE_OIDC_AUTH = 'UPDATE_OIDC_AUTH';
|
|
||||||
export const UPDATE_OIDC_AUTH_ERROR = 'UPDATE_OIDC_AUTH_ERROR';
|
|
||||||
|
|
||||||
const debug = require('debug')('unleash:e-admin-auth-actions');
|
|
||||||
|
|
||||||
export function getGoogleConfig() {
|
|
||||||
debug('Start fetching google-auth config');
|
|
||||||
return dispatch =>
|
|
||||||
api
|
|
||||||
.getGoogleConfig()
|
|
||||||
.then(config =>
|
|
||||||
dispatch({
|
|
||||||
type: RECIEVE_GOOGLE_CONFIG,
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(dispatchError(dispatch, RECIEVE_GOOGLE_CONFIG_ERROR));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateGoogleConfig(data) {
|
|
||||||
return dispatch =>
|
|
||||||
api
|
|
||||||
.updateGoogleConfig(data)
|
|
||||||
.then(config => dispatch({ type: UPDATE_GOOGLE_AUTH, config }))
|
|
||||||
.catch(e => {
|
|
||||||
dispatchError(dispatch, UPDATE_GOOGLE_AUTH_ERROR)(e);
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSamlConfig() {
|
|
||||||
debug('Start fetching Saml-auth config');
|
|
||||||
return dispatch =>
|
|
||||||
api
|
|
||||||
.getSamlConfig()
|
|
||||||
.then(config =>
|
|
||||||
dispatch({
|
|
||||||
type: RECIEVE_SAML_CONFIG,
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(dispatchError(dispatch, RECIEVE_SAML_CONFIG_ERROR));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateSamlConfig(data) {
|
|
||||||
return dispatch =>
|
|
||||||
api
|
|
||||||
.updateSamlConfig(data)
|
|
||||||
.then(config => dispatch({ type: UPDATE_SAML_AUTH, config }))
|
|
||||||
.catch(e => {
|
|
||||||
dispatchError(dispatch, UPDATE_SAML_AUTH_ERROR)(e);
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOidcConfig() {
|
|
||||||
debug('Start fetching OIDC-auth config');
|
|
||||||
return dispatch =>
|
|
||||||
api
|
|
||||||
.getOidcConfig()
|
|
||||||
.then(config =>
|
|
||||||
dispatch({
|
|
||||||
type: RECIEVE_OIDC_CONFIG,
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(dispatchError(dispatch, RECIEVE_OIDC_CONFIG_ERROR));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateOidcConfig(data) {
|
|
||||||
return dispatch =>
|
|
||||||
api
|
|
||||||
.updateOidcConfig(data)
|
|
||||||
.then(config => dispatch({ type: UPDATE_OIDC_AUTH, config }))
|
|
||||||
.catch(e => {
|
|
||||||
dispatchError(dispatch, UPDATE_OIDC_AUTH_ERROR)(e);
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
|
||||||
import { formatApiPath } from '../../utils/format-path';
|
|
||||||
|
|
||||||
const GOOGLE_URI = formatApiPath('api/admin/auth/google/settings');
|
|
||||||
const SAML_URI = formatApiPath('api/admin/auth/saml/settings');
|
|
||||||
const OIDC_URI = formatApiPath('api/admin/auth/oidc/settings');
|
|
||||||
|
|
||||||
function getGoogleConfig() {
|
|
||||||
return fetch(GOOGLE_URI, { headers, credentials: 'include' })
|
|
||||||
.then(throwIfNotSuccess)
|
|
||||||
.then(response => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateGoogleConfig(data) {
|
|
||||||
return fetch(GOOGLE_URI, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(throwIfNotSuccess)
|
|
||||||
.then(response => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSamlConfig() {
|
|
||||||
return fetch(SAML_URI, { headers, credentials: 'include' })
|
|
||||||
.then(throwIfNotSuccess)
|
|
||||||
.then(response => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSamlConfig(data) {
|
|
||||||
return fetch(SAML_URI, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(throwIfNotSuccess)
|
|
||||||
.then(response => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOidcConfig() {
|
|
||||||
return fetch(OIDC_URI, { headers, credentials: 'include' })
|
|
||||||
.then(throwIfNotSuccess)
|
|
||||||
.then(response => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateOidcConfig(data) {
|
|
||||||
return fetch(OIDC_URI, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(throwIfNotSuccess)
|
|
||||||
.then(response => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
getGoogleConfig,
|
|
||||||
updateGoogleConfig,
|
|
||||||
getSamlConfig,
|
|
||||||
updateSamlConfig,
|
|
||||||
getOidcConfig,
|
|
||||||
updateOidcConfig,
|
|
||||||
};
|
|
@ -1,20 +0,0 @@
|
|||||||
import { Map as $Map } from 'immutable';
|
|
||||||
import { RECIEVE_GOOGLE_CONFIG, UPDATE_GOOGLE_AUTH, RECIEVE_SAML_CONFIG, UPDATE_SAML_AUTH, UPDATE_OIDC_AUTH, RECIEVE_OIDC_CONFIG } from './actions';
|
|
||||||
|
|
||||||
const store = (state = new $Map({ google: {}, saml: {}, oidc: {} }), action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case UPDATE_GOOGLE_AUTH:
|
|
||||||
case RECIEVE_GOOGLE_CONFIG:
|
|
||||||
return state.set('google', action.config);
|
|
||||||
case UPDATE_SAML_AUTH:
|
|
||||||
case RECIEVE_SAML_CONFIG:
|
|
||||||
return state.set('saml', action.config);
|
|
||||||
case UPDATE_OIDC_AUTH:
|
|
||||||
case RECIEVE_OIDC_CONFIG:
|
|
||||||
return state.set('oidc', action.config);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default store;
|
|
@ -15,7 +15,6 @@ import uiConfig from './ui-config';
|
|||||||
import context from './context';
|
import context from './context';
|
||||||
import projects from './project';
|
import projects from './project';
|
||||||
import addons from './addons';
|
import addons from './addons';
|
||||||
import authAdmin from './e-admin-auth';
|
|
||||||
import apiCalls from './api-calls';
|
import apiCalls from './api-calls';
|
||||||
import invoiceAdmin from './e-admin-invoice';
|
import invoiceAdmin from './e-admin-invoice';
|
||||||
import feedback from './feedback';
|
import feedback from './feedback';
|
||||||
@ -37,7 +36,6 @@ const unleashStore = combineReducers({
|
|||||||
context,
|
context,
|
||||||
projects,
|
projects,
|
||||||
addons,
|
addons,
|
||||||
authAdmin,
|
|
||||||
apiCalls,
|
apiCalls,
|
||||||
invoiceAdmin,
|
invoiceAdmin,
|
||||||
feedback,
|
feedback,
|
||||||
|
8
frontend/src/utils/format-unknown-error.test.ts
Normal file
8
frontend/src/utils/format-unknown-error.test.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { formatUnknownError } from './format-unknown-error';
|
||||||
|
|
||||||
|
test('formatUnknownError', () => {
|
||||||
|
expect(formatUnknownError(1)).toEqual('Unknown error');
|
||||||
|
expect(formatUnknownError('1')).toEqual('1');
|
||||||
|
expect(formatUnknownError(new Error('1'))).toEqual('1');
|
||||||
|
expect(formatUnknownError(new Error())).toEqual('Error');
|
||||||
|
});
|
10
frontend/src/utils/format-unknown-error.ts
Normal file
10
frontend/src/utils/format-unknown-error.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Get a human-readable error message string from a caught value.
|
||||||
|
export const formatUnknownError = (error: unknown): string => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message || error.toString();
|
||||||
|
} else if (typeof error === 'string') {
|
||||||
|
return error;
|
||||||
|
} else {
|
||||||
|
return 'Unknown error';
|
||||||
|
}
|
||||||
|
};
|
11
frontend/src/utils/remove-empty-string-fields.test.ts
Normal file
11
frontend/src/utils/remove-empty-string-fields.test.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { removeEmptyStringFields } from './remove-empty-string-fields';
|
||||||
|
|
||||||
|
test('removeEmptyStringFields', () => {
|
||||||
|
expect(removeEmptyStringFields({})).toEqual({});
|
||||||
|
expect(removeEmptyStringFields({ a: undefined })).toEqual({ a: undefined });
|
||||||
|
expect(removeEmptyStringFields({ a: 0 })).toEqual({ a: 0 });
|
||||||
|
expect(removeEmptyStringFields({ a: 1 })).toEqual({ a: 1 });
|
||||||
|
expect(removeEmptyStringFields({ a: '1' })).toEqual({ a: '1' });
|
||||||
|
expect(removeEmptyStringFields({ a: '' })).toEqual({});
|
||||||
|
expect(removeEmptyStringFields({ a: '', b: '2' })).toEqual({ b: '2' });
|
||||||
|
});
|
9
frontend/src/utils/remove-empty-string-fields.ts
Normal file
9
frontend/src/utils/remove-empty-string-fields.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Remove fields from an object if their value is the empty string.
|
||||||
|
export const removeEmptyStringFields = (object: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
}): { [key: string]: unknown } => {
|
||||||
|
const entries = Object.entries(object);
|
||||||
|
const filtered = entries.filter(([, v]) => v !== '');
|
||||||
|
|
||||||
|
return Object.fromEntries(filtered);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user