mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
chec
This commit is contained in:
parent
41a974eb0b
commit
97b51cab02
@ -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<Map<String, String>> 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<Map<String, Object>> saveLicenseKey(
|
||||
@RequestBody Map<String, String> 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<String, Object> 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<Map<String, Object>> getLicenseInfo() {
|
||||
try {
|
||||
Map<String, Object> 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<StripeCheckoutProps> = ({
|
||||
email,
|
||||
onSuccess,
|
||||
onError,
|
||||
onLicenseActivated,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState<CheckoutState>({ status: 'idle' });
|
||||
@ -103,6 +105,27 @@ const StripeCheckout: React.FC<StripeCheckoutProps> = ({
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -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<string>('gbp');
|
||||
const [useStaticVersion, setUseStaticVersion] = useState(false);
|
||||
const [currentLicenseInfo, setCurrentLicenseInfo] = useState<any>(null);
|
||||
const [licenseInfoLoading, setLicenseInfoLoading] = useState(false);
|
||||
const [licenseInfoError, setLicenseInfoError] = useState<string | null>(null);
|
||||
const [showLicenseKey, setShowLicenseKey] = useState(false);
|
||||
const [email, setEmail] = useState<string>('');
|
||||
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 (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||
{/* License Information Display - Always visible */}
|
||||
<Alert
|
||||
color={licenseInfoLoading ? "gray" : licenseInfoError ? "red" : currentLicenseInfo?.enabled ? "green" : "blue"}
|
||||
title={t('admin.plan.licenseInfo', 'License Information')}
|
||||
>
|
||||
{licenseInfoLoading ? (
|
||||
<Group gap="xs">
|
||||
<Loader size="sm" />
|
||||
<Text size="sm">{t('admin.plan.loadingLicense', 'Loading license information...')}</Text>
|
||||
</Group>
|
||||
) : licenseInfoError ? (
|
||||
<Text size="sm" c="red">{t('admin.plan.licenseError', 'Failed to load license info')}: {licenseInfoError}</Text>
|
||||
) : currentLicenseInfo ? (
|
||||
<Stack gap="xs">
|
||||
<Text size="sm">
|
||||
<strong>{t('admin.plan.licenseType', 'License Type')}:</strong> {currentLicenseInfo.licenseType}
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
<strong>{t('admin.plan.status', 'Status')}:</strong> {currentLicenseInfo.enabled ? t('admin.plan.active', 'Active') : t('admin.plan.inactive', 'Inactive')}
|
||||
</Text>
|
||||
{currentLicenseInfo.licenseType === 'ENTERPRISE' && currentLicenseInfo.maxUsers > 0 && (
|
||||
<Text size="sm">
|
||||
<strong>{t('admin.plan.maxUsers', 'Max Users')}:</strong> {currentLicenseInfo.maxUsers}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
<Text size="sm">{t('admin.plan.noLicenseInfo', 'No license information available')}</Text>
|
||||
)}
|
||||
</Alert>
|
||||
|
||||
{/* Customer Information Section */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
@ -240,6 +295,7 @@ const AdminPlanSection: React.FC = () => {
|
||||
email={email}
|
||||
onSuccess={handlePaymentSuccess}
|
||||
onError={handlePaymentError}
|
||||
onLicenseActivated={handleLicenseActivated}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user