mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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