mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: license checker for self-hosted (#5239)
Show banner for enterprise self-hosted if they violate their license. --------- Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
parent
2a1c0616e0
commit
fef6935d3a
@ -22,6 +22,7 @@ import { styled } from '@mui/material';
|
|||||||
import { InitialRedirect } from './InitialRedirect';
|
import { InitialRedirect } from './InitialRedirect';
|
||||||
import { InternalBanners } from './banners/internalBanners/InternalBanners';
|
import { InternalBanners } from './banners/internalBanners/InternalBanners';
|
||||||
import { ExternalBanners } from './banners/externalBanners/ExternalBanners';
|
import { ExternalBanners } from './banners/externalBanners/ExternalBanners';
|
||||||
|
import { LicenseBanner } from './banners/internalBanners/LicenseBanner';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(() => ({
|
const StyledContainer = styled('div')(() => ({
|
||||||
'& ul': {
|
'& ul': {
|
||||||
@ -65,6 +66,7 @@ export const App = () => {
|
|||||||
)}
|
)}
|
||||||
show={<MaintenanceBanner />}
|
show={<MaintenanceBanner />}
|
||||||
/>
|
/>
|
||||||
|
<LicenseBanner />
|
||||||
<ExternalBanners />
|
<ExternalBanners />
|
||||||
<InternalBanners />
|
<InternalBanners />
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
|
@ -19,6 +19,7 @@ import NotFound from 'component/common/NotFound/NotFound';
|
|||||||
import { AdminIndex } from './AdminIndex';
|
import { AdminIndex } from './AdminIndex';
|
||||||
import { AdminTabsMenu } from './menu/AdminTabsMenu';
|
import { AdminTabsMenu } from './menu/AdminTabsMenu';
|
||||||
import { Banners } from './banners/Banners';
|
import { Banners } from './banners/Banners';
|
||||||
|
import { License } from './license/License';
|
||||||
|
|
||||||
export const Admin = () => {
|
export const Admin = () => {
|
||||||
return (
|
return (
|
||||||
@ -38,6 +39,7 @@ export const Admin = () => {
|
|||||||
<Route path='network/*' element={<Network />} />
|
<Route path='network/*' element={<Network />} />
|
||||||
<Route path='maintenance' element={<MaintenanceAdmin />} />
|
<Route path='maintenance' element={<MaintenanceAdmin />} />
|
||||||
<Route path='banners' element={<Banners />} />
|
<Route path='banners' element={<Banners />} />
|
||||||
|
<Route path='license' element={<License />} />
|
||||||
<Route path='cors' element={<CorsAdmin />} />
|
<Route path='cors' element={<CorsAdmin />} />
|
||||||
<Route path='auth' element={<AuthSettings />} />
|
<Route path='auth' element={<AuthSettings />} />
|
||||||
<Route
|
<Route
|
||||||
|
@ -86,6 +86,13 @@ export const adminRoutes: INavigationMenuItem[] = [
|
|||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
group: 'instance',
|
group: 'instance',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/license',
|
||||||
|
title: 'License',
|
||||||
|
menu: { adminSettings: true, mode: ['enterprise'] },
|
||||||
|
flag: 'enableLicense',
|
||||||
|
group: 'instance',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/instance-privacy',
|
path: '/admin/instance-privacy',
|
||||||
title: 'Instance privacy',
|
title: 'Instance privacy',
|
||||||
|
153
frontend/src/component/admin/license/License.tsx
Normal file
153
frontend/src/component/admin/license/License.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
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 useLicenseKeyApi from 'hooks/api/actions/useLicenseAPI/useLicenseApi';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
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 } = useLicenseKeyApi();
|
||||||
|
|
||||||
|
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>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!!license.token}
|
||||||
|
show={
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<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
|
||||||
|
/>
|
||||||
|
<Grid container spacing={3} marginTop={2}>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import { Banner } from 'component/banners/Banner/Banner';
|
import { Banner } from 'component/banners/Banner/Banner';
|
||||||
|
import { useLicenseCheck } from 'hooks/api/getters/useLicense/useLicense';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { useVariant } from 'hooks/useVariant';
|
import { useVariant } from 'hooks/useVariant';
|
||||||
import { IBanner } from 'interfaces/banner';
|
import { IBanner } from 'interfaces/banner';
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Banner } from 'component/banners/Banner/Banner';
|
||||||
|
import { useLicenseCheck } from 'hooks/api/getters/useLicense/useLicense';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { BannerVariant } from 'interfaces/banner';
|
||||||
|
|
||||||
|
export const LicenseBanner = () => {
|
||||||
|
const { isEnterprise } = useUiConfig();
|
||||||
|
const licenseInfo = useLicenseCheck();
|
||||||
|
|
||||||
|
// Only for enterprise
|
||||||
|
if (
|
||||||
|
isEnterprise() &&
|
||||||
|
licenseInfo &&
|
||||||
|
!licenseInfo.isValid &&
|
||||||
|
!licenseInfo.loading &&
|
||||||
|
!licenseInfo.error
|
||||||
|
) {
|
||||||
|
const banner = {
|
||||||
|
message:
|
||||||
|
licenseInfo.message || 'You have an invalid Unleash license.',
|
||||||
|
variant: 'error' as BannerVariant,
|
||||||
|
sticky: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Banner key={banner.message} banner={banner} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
@ -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('Did not receive a response from the server.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const useLicenseKeyApi = () => {
|
||||||
|
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 useLicenseKeyApi;
|
67
frontend/src/hooks/api/getters/useLicense/useLicense.ts
Normal file
67
frontend/src/hooks/api/getters/useLicense/useLicense.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
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());
|
||||||
|
};
|
@ -71,6 +71,7 @@ export type UiFlags = {
|
|||||||
celebrateUnleash?: boolean;
|
celebrateUnleash?: boolean;
|
||||||
increaseUnleashWidth?: boolean;
|
increaseUnleashWidth?: boolean;
|
||||||
featureSearchFeedback?: boolean;
|
featureSearchFeedback?: boolean;
|
||||||
|
enableLicense?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
Loading…
Reference in New Issue
Block a user