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 { InternalBanners } from './banners/internalBanners/InternalBanners';
|
||||
import { ExternalBanners } from './banners/externalBanners/ExternalBanners';
|
||||
import { LicenseBanner } from './banners/internalBanners/LicenseBanner';
|
||||
|
||||
const StyledContainer = styled('div')(() => ({
|
||||
'& ul': {
|
||||
@ -65,6 +66,7 @@ export const App = () => {
|
||||
)}
|
||||
show={<MaintenanceBanner />}
|
||||
/>
|
||||
<LicenseBanner />
|
||||
<ExternalBanners />
|
||||
<InternalBanners />
|
||||
<StyledContainer>
|
||||
|
@ -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 (
|
||||
@ -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
|
||||
|
@ -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',
|
||||
|
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 { 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';
|
||||
|
@ -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;
|
||||
increaseUnleashWidth?: boolean;
|
||||
featureSearchFeedback?: boolean;
|
||||
enableLicense?: boolean;
|
||||
};
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
Loading…
Reference in New Issue
Block a user