Can remove license without rebooting backend

This commit is contained in:
Connor Yoh
2025-11-19 14:01:18 +00:00
parent 83a212ec16
commit a868b37777
2 changed files with 81 additions and 71 deletions

View File

@@ -83,7 +83,8 @@ public class AdminLicenseController {
@RequestBody Map<String, String> request) {
String licenseKey = request.get("licenseKey");
if (licenseKey == null || licenseKey.trim().isEmpty()) {
// Reject null but allow empty string to clear license
if (licenseKey == null) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "error", "License key is required"));
}
@@ -97,6 +98,7 @@ public class AdminLicenseController {
applicationProperties.getPremium().setEnabled(true);
// Use existing LicenseKeyChecker to update and validate license
// Empty string will be evaluated as NORMAL license (free tier)
licenseKeyChecker.updateLicenseKey(licenseKey.trim());
// Get current license status
@@ -106,6 +108,12 @@ public class AdminLicenseController {
if (license != License.NORMAL) {
GeneralUtils.saveKeyToSettings("premium.enabled", true);
// Enable premium features
// Save maxUsers from license metadata
Integer maxUsers = applicationProperties.getPremium().getMaxUsers();
if (maxUsers != null) {
GeneralUtils.saveKeyToSettings("premium.maxUsers", maxUsers);
}
} else {
GeneralUtils.saveKeyToSettings("premium.enabled", false);
log.info("License key is not valid for premium features: type={}", license.name());

View File

@@ -2,50 +2,27 @@ import React, { useState, useCallback, useEffect } from 'react';
import { Divider, Loader, Alert, Select, Group, Text, Collapse, Button, TextInput, Stack, Paper } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { usePlans } from '@app/hooks/usePlans';
import { PlanTierGroup } from '@app/services/licenseService';
import licenseService, { PlanTierGroup } from '@app/services/licenseService';
import { useCheckout } from '@app/contexts/CheckoutContext';
import { useLicense } from '@app/contexts/LicenseContext';
import AvailablePlansSection from '@app/components/shared/config/configSections/plan/AvailablePlansSection';
import StaticPlanSection from '@app/components/shared/config/configSections/plan/StaticPlanSection';
import { useAppConfig } from '@app/contexts/AppConfigContext';
import { alert } from '@app/components/toast';
import LocalIcon from '@app/components/shared/LocalIcon';
import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal';
import { useRestartServer } from '@app/components/shared/config/useRestartServer';
import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge';
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
import { ManageBillingButton } from '@app/components/shared/ManageBillingButton';
interface PremiumSettingsData {
key?: string;
enabled?: boolean;
}
const AdminPlanSection: React.FC = () => {
const { t } = useTranslation();
const { config } = useAppConfig();
const { openCheckout } = useCheckout();
const { licenseInfo } = useLicense();
const { licenseInfo, refetchLicense } = useLicense();
const [currency, setCurrency] = useState<string>('gbp');
const [useStaticVersion, setUseStaticVersion] = useState(false);
const [showLicenseKey, setShowLicenseKey] = useState(false);
const [licenseKeyInput, setLicenseKeyInput] = useState<string>('');
const [savingLicense, setSavingLicense] = useState(false);
const { plans, loading, error, refetch } = usePlans(currency);
// Premium/License key management
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const {
settings: premiumSettings,
setSettings: setPremiumSettings,
loading: premiumLoading,
saving: premiumSaving,
fetchSettings: fetchPremiumSettings,
saveSettings: savePremiumSettings,
isFieldPending,
} = useAdminSettings<PremiumSettingsData>({
sectionName: 'premium',
});
// Check if we should use static version
useEffect(() => {
// Check if Stripe is configured
@@ -53,21 +30,42 @@ const AdminPlanSection: React.FC = () => {
if (!stripeKey || error) {
setUseStaticVersion(true);
}
// Fetch premium settings
fetchPremiumSettings();
}, [error, config, fetchPremiumSettings]);
}, [error]);
const handleSaveLicense = async () => {
try {
await savePremiumSettings();
showRestartModal();
} catch (_error) {
setSavingLicense(true);
// Allow empty string to clear/remove license
const response = await licenseService.saveLicenseKey(licenseKeyInput.trim());
if (response.success) {
// Refresh license context to update all components
await refetchLicense();
alert({
alertType: 'success',
title: t('admin.settings.premium.key.success', 'License Key Saved'),
body: t('admin.settings.premium.key.successMessage', 'Your license key has been activated successfully. No restart required.'),
});
// Clear input
setLicenseKeyInput('');
} else {
alert({
alertType: 'error',
title: t('admin.error', 'Error'),
body: response.error || t('admin.settings.saveError', 'Failed to save license key'),
});
}
} catch (error) {
console.error('Failed to save license key:', error);
alert({
alertType: 'error',
title: t('admin.error', 'Error'),
body: t('admin.settings.saveError', 'Failed to save settings'),
body: t('admin.settings.saveError', 'Failed to save license key'),
});
} finally {
setSavingLicense(false);
}
};
@@ -190,46 +188,50 @@ const AdminPlanSection: React.FC = () => {
</Text>
</Alert>
{premiumLoading ? (
<Stack align="center" justify="center" h={100}>
<Loader size="md" />
</Stack>
) : (
<Paper withBorder p="md" radius="md">
<Stack gap="md">
<div>
<TextInput
label={
<Group gap="xs">
<span>{t('admin.settings.premium.key.label', 'License Key')}</span>
<PendingBadge show={isFieldPending('key')} />
</Group>
}
description={t('admin.settings.premium.key.description', 'Enter your premium or enterprise license key. Premium features will be automatically enabled when a key is provided.')}
value={premiumSettings.key || ''}
onChange={(e) => setPremiumSettings({ ...premiumSettings, key: e.target.value })}
placeholder="00000000-0000-0000-0000-000000000000"
/>
</div>
<Group justify="flex-end">
<Button onClick={handleSaveLicense} loading={premiumSaving} size="sm">
{t('admin.settings.save', 'Save Changes')}
</Button>
</Group>
{/* Severe warning if license already exists */}
{licenseInfo?.licenseKey && (
<Alert
variant="light"
color="red"
icon={<LocalIcon icon="warning-rounded" width="1rem" height="1rem" />}
title={t('admin.settings.premium.key.overwriteWarning.title', '⚠️ Warning: Existing License Detected')}
>
<Stack gap="xs">
<Text size="sm" fw={600}>
{t('admin.settings.premium.key.overwriteWarning.line1', 'Overwriting your current license key cannot be undone.')}
</Text>
<Text size="sm">
{t('admin.settings.premium.key.overwriteWarning.line2', 'Your previous license will be permanently lost unless you have backed it up elsewhere.')}
</Text>
<Text size="sm" fw={500}>
{t('admin.settings.premium.key.overwriteWarning.line3', 'Important: Keep license keys private and secure. Never share them publicly.')}
</Text>
</Stack>
</Paper>
</Alert>
)}
<Paper withBorder p="md" radius="md">
<Stack gap="md">
<TextInput
label={t('admin.settings.premium.key.label', 'License Key')}
description={t('admin.settings.premium.key.description', 'Enter your premium or enterprise license key. Premium features will be automatically enabled when a key is provided.')}
value={licenseKeyInput}
onChange={(e) => setLicenseKeyInput(e.target.value)}
placeholder="00000000-0000-0000-0000-000000000000"
type="password"
disabled={savingLicense}
/>
<Group justify="flex-end">
<Button onClick={handleSaveLicense} loading={savingLicense} size="sm">
{t('admin.settings.save', 'Save Changes')}
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Collapse>
</div>
{/* Restart Confirmation Modal */}
<RestartConfirmationModal
opened={restartModalOpened}
onClose={closeRestartModal}
onRestart={restartServer}
/>
</div>
);
};