From a868b377770064ec108ed51babd9cac9fbe6ee76 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Wed, 19 Nov 2025 14:01:18 +0000 Subject: [PATCH] Can remove license without rebooting backend --- .../api/AdminLicenseController.java | 10 +- .../configSections/AdminPlanSection.tsx | 142 +++++++++--------- 2 files changed, 81 insertions(+), 71 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminLicenseController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminLicenseController.java index 05255ef07..7bd5836c8 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminLicenseController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminLicenseController.java @@ -83,7 +83,8 @@ public class AdminLicenseController { @RequestBody Map 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()); diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx index b8a7644e3..fdcd492d8 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx @@ -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('gbp'); const [useStaticVersion, setUseStaticVersion] = useState(false); const [showLicenseKey, setShowLicenseKey] = useState(false); + const [licenseKeyInput, setLicenseKeyInput] = useState(''); + 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({ - 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 = () => { - {premiumLoading ? ( - - - - ) : ( - - -
- - {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={premiumSettings.key || ''} - onChange={(e) => setPremiumSettings({ ...premiumSettings, key: e.target.value })} - placeholder="00000000-0000-0000-0000-000000000000" - /> -
- - - - + {/* Severe warning if license already exists */} + {licenseInfo?.licenseKey && ( + } + title={t('admin.settings.premium.key.overwriteWarning.title', '⚠️ Warning: Existing License Detected')} + > + + + {t('admin.settings.premium.key.overwriteWarning.line1', 'Overwriting your current license key cannot be undone.')} + + + {t('admin.settings.premium.key.overwriteWarning.line2', 'Your previous license will be permanently lost unless you have backed it up elsewhere.')} + + + {t('admin.settings.premium.key.overwriteWarning.line3', 'Important: Keep license keys private and secure. Never share them publicly.')} + -
+ )} + + + + setLicenseKeyInput(e.target.value)} + placeholder="00000000-0000-0000-0000-000000000000" + type="password" + disabled={savingLicense} + /> + + + + + + - - {/* Restart Confirmation Modal */} - ); };