Skip to content

Commit

Permalink
wip: license checker for self-hosted
Browse files Browse the repository at this point in the history
  • Loading branch information
ivarconr committed Dec 4, 2023
1 parent 08204e5 commit 1ed2dca
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 1 deletion.
2 changes: 2 additions & 0 deletions frontend/src/component/admin/Admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import NotFound from 'component/common/NotFound/NotFound';
import { AdminIndex } from './AdminIndex';
import { AdminTabsMenu } from './menu/AdminTabsMenu';
import { Banners } from './banners/Banners';
import { License } from './license/License';

export const Admin = () => {
return (
Expand All @@ -38,6 +39,7 @@ export const Admin = () => {
<Route path='network/*' element={<Network />} />
<Route path='maintenance' element={<MaintenanceAdmin />} />
<Route path='banners' element={<Banners />} />
<Route path='license' element={<License />} />
<Route path='cors' element={<CorsAdmin />} />
<Route path='auth' element={<AuthSettings />} />
<Route
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/component/admin/adminRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export const adminRoutes: INavigationMenuItem[] = [
menu: { adminSettings: true },
group: 'instance',
},
{
path: '/admin/license',
title: 'License',
menu: { adminSettings: true, mode: ['enterprise'] },
flag: 'enableLicense',
group: 'instance',
},
{
path: '/admin/instance-privacy',
title: 'Instance privacy',
Expand Down
140 changes: 140 additions & 0 deletions frontend/src/component/admin/license/License.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { Box, Button, Grid, TextField, styled } from '@mui/material';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useLicense, useLicenseCheck } from 'hooks/api/getters/useLicense/useLicense';
import { formatDateYMD } from 'utils/formatDate';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { useState } from 'react';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import useUpdateLicenseKeyApi from 'hooks/api/actions/useLicenseAPI/useLicenseApi';


const StyledBox = styled(Box)(({ theme }) => ({
display: 'grid',
gap: theme.spacing(4),
}));

const StyledDataCollectionPropertyRow = styled('div')(() => ({
display: 'table-row',
}));

const StyledPropertyName = styled('p')(({ theme }) => ({
display: 'table-cell',
fontWeight: theme.fontWeight.bold,
paddingTop: theme.spacing(2),
}));

const StyledPropertyDetails = styled('p')(({ theme }) => ({
display: 'table-cell',
paddingTop: theme.spacing(2),
paddingLeft: theme.spacing(4),
}));





export const License = () => {
const { setToastData, setToastApiError } = useToast();
const { license, error, refetchLicense } = useLicense();
const { reCheckLicense } = useLicenseCheck();
const { loading } = useUiConfig();
const { locationSettings } = useLocationSettings();
const [token, setToken] = useState('');
const { updateLicenseKey } = useUpdateLicenseKeyApi();

const updateToken = (event: React.ChangeEvent<HTMLInputElement>) => {
setToken(event.target.value.trim());
};

if (loading || !license) {
return null;
}

const onSubmit = async (event: React.SyntheticEvent) => {
event.preventDefault();

try {
await updateLicenseKey(token);
setToastData({
title: 'License key updated',
type: 'success',
});
refetchLicense();
reCheckLicense ();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};

return (
<PageContent header={<PageHeader title='Unleash Enterprise License' />}>
<StyledBox>
{license.token ? <div>
<StyledDataCollectionPropertyRow>
<StyledPropertyName>Customer</StyledPropertyName>
<StyledPropertyDetails>
{license?.customer}
</StyledPropertyDetails>
</StyledDataCollectionPropertyRow>
<StyledDataCollectionPropertyRow>
<StyledPropertyName>Plan</StyledPropertyName>
<StyledPropertyDetails>
{license?.plan}
</StyledPropertyDetails>
</StyledDataCollectionPropertyRow>
<StyledDataCollectionPropertyRow>
<StyledPropertyName>Seats</StyledPropertyName>
<StyledPropertyDetails>
{license?.seats}
</StyledPropertyDetails>
</StyledDataCollectionPropertyRow>
<StyledDataCollectionPropertyRow>
<StyledPropertyName>Expire at</StyledPropertyName>
<StyledPropertyDetails>
{formatDateYMD(license.expireAt, locationSettings.locale)}
</StyledPropertyDetails>
</StyledDataCollectionPropertyRow>
</div>: <p>You do not have a registered Unleash Enterprise License.</p>}


<form onSubmit={onSubmit}>
<TextField
onChange={updateToken}
label='New License Key'
name='licenseKey'
value={token}
style={{ width: '100%' }}
variant='outlined'
size='small'
multiline
rows={6}
required
/>
<br /><br />

<Grid container spacing={3}>
<Grid item md={5}>
<Button
variant='contained'
color='primary'
type='submit'
disabled={loading}
>
Update License Key
</Button>{' '}
<p>
<small style={{ color: 'red' }}>
{error?.message}
</small>
</p>
</Grid>
</Grid>
</form>

</StyledBox>
</PageContent>
);
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Banner } from 'component/banners/Banner/Banner';
import { useLicenseCheck } from 'hooks/api/getters/useLicense/useLicense';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useVariant } from 'hooks/useVariant';
import { IBanner } from 'interfaces/banner';

export const ExternalBanners = () => {
const { uiConfig } = useUiConfig();
const { uiConfig, isEnterprise } = useUiConfig();
const licenseInfo = useLicenseCheck();

const bannerVariantFromMessageBannerFlag = useVariant<IBanner | IBanner[]>(
uiConfig.flags.messageBanner,
Expand All @@ -19,6 +21,17 @@ export const ExternalBanners = () => {
const banners: IBanner[] = Array.isArray(bannerVariant)
? bannerVariant
: [bannerVariant];

// Only for enterprise
if(isEnterprise()) {
if(licenseInfo && !licenseInfo.isValid && !licenseInfo.loading && !licenseInfo.error) {
banners.push({
message: licenseInfo.message || 'You have an invalid Unleash license.',
variant: 'error',
sticky: true,
});
}
}

return (
<>
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/hooks/api/actions/useLicenseAPI/useLicenseApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Dispatch, SetStateAction } from 'react';
import useAPI from '../useApi/useApi';

export const handleBadRequest = async (
setErrors?: Dispatch<SetStateAction<{}>>,
res?: Response,
) => {
if (!setErrors) return;
if (res) {
const data = await res.json();
setErrors({ message: data.message });
throw new Error(data.message);
}

throw new Error();
};

const useUpdateLicenseKeyApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true,
handleBadRequest,
});

const updateLicenseKey = async (token: string): Promise<void> => {
const path = `api/admin/license`;
const req = createRequest(path, {
method: 'POST',
body: JSON.stringify({ token }),
});

await makeRequest(req.caller, req.id);
};

return { updateLicenseKey, errors, loading };
};

export default useUpdateLicenseKeyApi;
70 changes: 70 additions & 0 deletions frontend/src/hooks/api/getters/useLicense/useLicense.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import useSWR from 'swr';
import { useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import useUiConfig from '../useUiConfig/useUiConfig';
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR';

export interface LicenseInfo {
isValid: boolean;
message?: string;
loading: boolean;
reCheckLicense: () => void;
error?: Error;
}

const fallback = {
isValid: true,
message: '',
loading: false,
};

export interface License {
license?: {
token: string;
customer: string;
plan: string;
seats: number;
expireAt: Date;
};
loading: boolean;
refetchLicense: () => void;
error?: Error;
}

export const useLicenseCheck = (): LicenseInfo => {
const { data, error, mutate } = useEnterpriseSWR(
fallback,
formatApiPath(`api/admin/license/check`),
fetcher,
);

return {
isValid: data?.isValid,
message: data?.message,
loading: !error && !data,
reCheckLicense: () => mutate(),
error,
};
};

export const useLicense = (): License => {
const { data, error, mutate } = useSWR(
formatApiPath(`api/admin/license`),
fetcher,
);

return {
license: { ...data },
loading: !error && !data,
refetchLicense: () => mutate(),
error,
};
};

const fetcher = (path: string) => {
return fetch(path)
.then(handleErrorResponses('License'))
.then((res) => res.json());
};
4 changes: 4 additions & 0 deletions frontend/src/interfaces/uiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export type UiFlags = {
scheduledConfigurationChanges?: boolean;
featureSearchAPI?: boolean;
featureSearchFrontend?: boolean;
internalMessageBanners?: boolean;
disableEnvsOnRevive?: boolean;
playgroundImprovements?: boolean;
enableLicense?: boolean;
};

export interface IVersionInfo {
Expand Down

0 comments on commit 1ed2dca

Please sign in to comment.