From 97b51cab024bc1d1369329b8537a3f443be39da0 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Sun, 16 Nov 2025 16:05:08 +0000 Subject: [PATCH] chec --- .../api/AdminLicenseController.java | 107 +++++++++++++++++- .../core/components/shared/StripeCheckout.tsx | 23 ++++ .../configSections/AdminPlanSection.tsx | 60 +++++++++- frontend/src/core/services/licenseService.ts | 29 +++++ 4 files changed, 215 insertions(+), 4 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AdminLicenseController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AdminLicenseController.java index cd782cdd1..fdad57b95 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AdminLicenseController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AdminLicenseController.java @@ -1,10 +1,14 @@ package stirling.software.proprietary.controller.api; +import java.util.HashMap; import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -13,10 +17,14 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.util.GeneralUtils; +import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License; +import stirling.software.proprietary.security.configuration.ee.LicenseKeyChecker; /** - * Admin controller for license management. Provides installation ID for Stripe checkout metadata. + * Admin controller for license management. Provides installation ID for Stripe checkout metadata + * and endpoints for managing license keys. */ @RestController @Slf4j @@ -25,6 +33,11 @@ import stirling.software.common.util.GeneralUtils; @Tag(name = "Admin License Management", description = "Admin-only License Management APIs") public class AdminLicenseController { + @Autowired(required = false) + private LicenseKeyChecker licenseKeyChecker; + + @Autowired private ApplicationProperties applicationProperties; + /** * Get the installation ID (machine fingerprint) for this self-hosted instance. This ID is used * as metadata in Stripe checkout to link licenses to specific installations. @@ -37,7 +50,6 @@ public class AdminLicenseController { description = "Returns the unique installation ID (MAC-based fingerprint) for this" + " self-hosted instance") - @PreAuthorize("hasRole('ROLE_ADMIN')") public ResponseEntity> getInstallationId() { try { String installationId = GeneralUtils.generateMachineFingerprint(); @@ -49,4 +61,95 @@ public class AdminLicenseController { .body(Map.of("error", "Failed to generate installation ID")); } } + + /** + * Save and activate a license key. This endpoint accepts a license key from the frontend (e.g., + * after Stripe checkout) and activates it on the backend. + * + * @param request Map containing the license key + * @return Response with success status, license type, and whether restart is required + */ + @PostMapping("/license-key") + @Operation( + summary = "Save and activate license key", + description = + "Accepts a license key and activates it on the backend. Returns the activated" + + " license type.") + public ResponseEntity> saveLicenseKey( + @RequestBody Map request) { + String licenseKey = request.get("licenseKey"); + + if (licenseKey == null || licenseKey.trim().isEmpty()) { + return ResponseEntity.badRequest() + .body(Map.of("success", false, "error", "License key is required")); + } + + try { + if (licenseKeyChecker == null) { + return ResponseEntity.internalServerError() + .body(Map.of("success", false, "error", "License checker not available")); + } + + // Use existing LicenseKeyChecker to update and validate license + licenseKeyChecker.updateLicenseKey(licenseKey.trim()); + + // Get current license status + License license = licenseKeyChecker.getPremiumLicenseEnabledResult(); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("licenseType", license.name()); + response.put("requiresRestart", false); // Dynamic evaluation works + response.put("message", "License key saved and activated"); + + log.info("License key saved and activated: type={}", license.name()); + + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("Failed to save license key", e); + return ResponseEntity.badRequest() + .body( + Map.of( + "success", + false, + "error", + "Failed to activate license: " + e.getMessage())); + } + } + + /** + * Get information about the current license key status, including license type, enabled status, + * and max users. + * + * @return Map containing license information + */ + @GetMapping("/license-info") + @Operation( + summary = "Get license information", + description = + "Returns information about the current license including type, enabled status," + + " and max users") + public ResponseEntity> getLicenseInfo() { + try { + Map response = new HashMap<>(); + + if (licenseKeyChecker != null) { + License license = licenseKeyChecker.getPremiumLicenseEnabledResult(); + response.put("licenseType", license.name()); + } else { + response.put("licenseType", License.NORMAL.name()); + } + + ApplicationProperties.Premium premium = applicationProperties.getPremium(); + response.put("enabled", premium.isEnabled()); + response.put("maxUsers", premium.getMaxUsers()); + response.put("hasKey", premium.getKey() != null && !premium.getKey().trim().isEmpty()); + + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("Failed to get license info", e); + return ResponseEntity.internalServerError() + .body(Map.of("error", "Failed to retrieve license information")); + } + } } diff --git a/frontend/src/core/components/shared/StripeCheckout.tsx b/frontend/src/core/components/shared/StripeCheckout.tsx index ab54a1905..88c3bbe5c 100644 --- a/frontend/src/core/components/shared/StripeCheckout.tsx +++ b/frontend/src/core/components/shared/StripeCheckout.tsx @@ -16,6 +16,7 @@ interface StripeCheckoutProps { email: string; onSuccess?: (sessionId: string) => void; onError?: (error: string) => void; + onLicenseActivated?: (licenseInfo: {licenseType: string; enabled: boolean; maxUsers: number; hasKey: boolean}) => void; } type CheckoutState = { @@ -32,6 +33,7 @@ const StripeCheckout: React.FC = ({ email, onSuccess, onError, + onLicenseActivated, }) => { const { t } = useTranslation(); const [state, setState] = useState({ status: 'idle' }); @@ -103,6 +105,27 @@ const StripeCheckout: React.FC = ({ if (response.status === 'ready' && response.license_key) { setLicenseKey(response.license_key); setPollingStatus('ready'); + + // Save license key to backend + try { + const saveResponse = await licenseService.saveLicenseKey(response.license_key); + if (saveResponse.success) { + console.log(`License key activated on backend: ${saveResponse.licenseType}`); + + // Fetch and pass license info to parent + try { + const licenseInfo = await licenseService.getLicenseInfo(); + onLicenseActivated?.(licenseInfo); + } catch (infoError) { + console.error('Error fetching license info:', infoError); + } + } else { + console.error('Failed to save license key to backend:', saveResponse.error); + } + } catch (error) { + console.error('Error saving license key to backend:', error); + } + return; } diff --git a/frontend/src/core/components/shared/config/configSections/AdminPlanSection.tsx b/frontend/src/core/components/shared/config/configSections/AdminPlanSection.tsx index cbb48e6d4..6ae4e2294 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminPlanSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/AdminPlanSection.tsx @@ -2,7 +2,7 @@ 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 StripeCheckout from '@app/components/shared/StripeCheckout'; import AvailablePlansSection from './plan/AvailablePlansSection'; import ActivePlanSection from './plan/ActivePlanSection'; @@ -15,6 +15,7 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; +import { convertOperationConfig } from '@app/hooks/tools/convert/useConvertOperation'; interface PremiumSettingsData { key?: string; @@ -29,6 +30,8 @@ const AdminPlanSection: React.FC = () => { const [currency, setCurrency] = useState('gbp'); const [useStaticVersion, setUseStaticVersion] = useState(false); const [currentLicenseInfo, setCurrentLicenseInfo] = useState(null); + const [licenseInfoLoading, setLicenseInfoLoading] = useState(false); + const [licenseInfoError, setLicenseInfoError] = useState(null); const [showLicenseKey, setShowLicenseKey] = useState(false); const [email, setEmail] = useState(''); const { plans, currentSubscription, loading, error, refetch } = usePlans(currency); @@ -51,6 +54,7 @@ const AdminPlanSection: React.FC = () => { useEffect(() => { const fetchLicenseInfo = async () => { try { + console.log('Fetching user and license info for plan section'); const adminData = await userManagementService.getUsers(); // Determine plan name based on config flags @@ -66,17 +70,32 @@ const AdminPlanSection: React.FC = () => { maxUsers: adminData.maxAllowedUsers, grandfathered: adminData.grandfatheredUserCount > 0, }); + + // Also fetch license info from new backend endpoint + try { + setLicenseInfoLoading(true); + setLicenseInfoError(null); + const backendLicenseInfo = await licenseService.getLicenseInfo(); + setCurrentLicenseInfo(backendLicenseInfo); + setLicenseInfoLoading(false); + } catch (licenseErr: any) { + console.error('Failed to fetch backend license info:', licenseErr); + setLicenseInfoLoading(false); + setLicenseInfoError(licenseErr?.response?.data?.error || licenseErr?.message || 'Unknown error'); + // Don't overwrite existing info if backend call fails + } } catch (err) { console.error('Failed to fetch license info:', err); } + }; // Check if Stripe is configured const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY; if (!stripeKey || error) { setUseStaticVersion(true); - fetchLicenseInfo(); } + fetchLicenseInfo(); // Fetch premium settings fetchPremiumSettings(); @@ -150,6 +169,11 @@ const AdminPlanSection: React.FC = () => { // Error is already displayed in the StripeCheckout component }, []); + const handleLicenseActivated = useCallback((licenseInfo: {licenseType: string; enabled: boolean; maxUsers: number; hasKey: boolean}) => { + console.log('License activated:', licenseInfo); + setCurrentLicenseInfo(licenseInfo); + }, []); + const handleCheckoutClose = useCallback(() => { setCheckoutOpen(false); setSelectedPlanGroup(null); @@ -187,6 +211,37 @@ const AdminPlanSection: React.FC = () => { return (
+ {/* License Information Display - Always visible */} + + {licenseInfoLoading ? ( + + + {t('admin.plan.loadingLicense', 'Loading license information...')} + + ) : licenseInfoError ? ( + {t('admin.plan.licenseError', 'Failed to load license info')}: {licenseInfoError} + ) : currentLicenseInfo ? ( + + + {t('admin.plan.licenseType', 'License Type')}: {currentLicenseInfo.licenseType} + + + {t('admin.plan.status', 'Status')}: {currentLicenseInfo.enabled ? t('admin.plan.active', 'Active') : t('admin.plan.inactive', 'Inactive')} + + {currentLicenseInfo.licenseType === 'ENTERPRISE' && currentLicenseInfo.maxUsers > 0 && ( + + {t('admin.plan.maxUsers', 'Max Users')}: {currentLicenseInfo.maxUsers} + + )} + + ) : ( + {t('admin.plan.noLicenseInfo', 'No license information available')} + )} + + {/* Customer Information Section */} @@ -240,6 +295,7 @@ const AdminPlanSection: React.FC = () => { email={email} onSuccess={handlePaymentSuccess} onError={handlePaymentError} + onLicenseActivated={handleLicenseActivated} /> )} diff --git a/frontend/src/core/services/licenseService.ts b/frontend/src/core/services/licenseService.ts index 070d09f83..e4fb9e945 100644 --- a/frontend/src/core/services/licenseService.ts +++ b/frontend/src/core/services/licenseService.ts @@ -429,6 +429,35 @@ const licenseService = { return data as LicenseKeyResponse; }, + + /** + * Save license key to backend + */ + async saveLicenseKey(licenseKey: string): Promise<{success: boolean; licenseType?: string; message?: string; error?: string}> { + try { + const response = await apiClient.post('/api/v1/admin/license-key', { + licenseKey: licenseKey, + }); + + return response.data; + } catch (error) { + console.error('Error saving license key:', error); + throw error; + } + }, + + /** + * Get current license information from backend + */ + async getLicenseInfo(): Promise<{licenseType: string; enabled: boolean; maxUsers: number; hasKey: boolean}> { + try { + const response = await apiClient.get('/api/v1/admin/license-info'); + return response.data; + } catch (error) { + console.error('Error fetching license info:', error); + throw error; + } + }, }; export default licenseService;