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