public resync license

http license
translations
This commit is contained in:
Connor Yoh 2025-11-19 12:10:48 +00:00
parent 920391f38a
commit afa71c002b
9 changed files with 293 additions and 113 deletions

View File

@ -118,6 +118,11 @@ public class LicenseKeyChecker {
synchronizeLicenseSettings();
}
public void resyncLicense() {
evaluateLicense();
synchronizeLicenseSettings();
}
public License getPremiumLicenseEnabledResult() {
return premiumEnabledResult;
}

View File

@ -143,6 +143,66 @@ public class AdminLicenseController {
}
}
/**
* Resync the current license with Keygen. This endpoint re-validates the existing license key
* and updates the max users setting. Used after subscription upgrades to sync the new license
* limits.
*
* @return Response with updated license information
*/
@PostMapping("/license/resync")
@Operation(
summary = "Resync license with Keygen",
description =
"Re-validates the existing license key with Keygen and updates local settings."
+ " Used after subscription upgrades.")
public ResponseEntity<Map<String, Object>> resyncLicense() {
try {
if (licenseKeyChecker == null) {
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "error", "License checker not available"));
}
String currentKey = applicationProperties.getPremium().getKey();
if (currentKey == null || currentKey.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "error", "No license key configured"));
}
log.info("Resyncing license with Keygen");
// Re-validate license and sync settings
licenseKeyChecker.resyncLicense();
// Get updated license status
License license = licenseKeyChecker.getPremiumLicenseEnabledResult();
ApplicationProperties.Premium premium = applicationProperties.getPremium();
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("licenseType", license.name());
response.put("enabled", premium.isEnabled());
response.put("maxUsers", premium.getMaxUsers());
response.put("message", "License resynced successfully");
log.info(
"License resynced: type={}, maxUsers={}",
license.name(),
premium.getMaxUsers());
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to resync license", e);
return ResponseEntity.internalServerError()
.body(
Map.of(
"success",
false,
"error",
"Failed to resync license: " + e.getMessage()));
}
}
/**
* Get information about the current license key status, including license type, enabled status,
* and max users.

View File

@ -5156,7 +5156,32 @@
"success": "Payment Successful!",
"successMessage": "Your subscription has been activated successfully. You will receive a confirmation email shortly.",
"autoClose": "This window will close automatically...",
"error": "Payment Error"
"error": "Payment Error",
"upgradeSuccess": "Payment successful! Your subscription has been upgraded. The license has been updated on your server. You will receive a confirmation email shortly.",
"paymentSuccess": "Payment successful! Retrieving your license key...",
"licenseActivated": "License activated! Your license key has been saved. A confirmation email has been sent to your registered email address.",
"licenseDelayed": "Payment successful! Your license is being generated. You will receive an email with your license key shortly. If you don't receive it within 10 minutes, please contact support.",
"licensePollingError": "Payment successful but we couldn't retrieve your license key automatically. Please check your email or contact support with your payment confirmation.",
"licenseRetrievalError": "Payment successful but license retrieval failed. You will receive your license key via email. Please contact support if you don't receive it within 10 minutes.",
"syncError": "Payment successful but license sync failed. Your license will be updated shortly. Please contact support if issues persist.",
"licenseSaveError": "Failed to save license key. Please contact support with your license key to complete activation.",
"paymentCanceled": "Payment was canceled. No charges were made.",
"syncingLicense": "Syncing your upgraded license...",
"generatingLicense": "Generating your license key...",
"upgradeComplete": "Upgrade Complete",
"upgradeCompleteMessage": "Your subscription has been upgraded successfully. Your existing license key has been updated.",
"stripeNotConfigured": "Stripe Not Configured",
"stripeNotConfiguredMessage": "Stripe payment integration is not configured. Please contact your administrator.",
"monthly": "Monthly",
"yearly": "Yearly",
"billingPeriod": "Billing Period",
"enterpriseNote": "Seats can be adjusted in checkout (1-1000).",
"installationId": "Installation ID",
"licenseKey": "Your License Key",
"licenseInstructions": "Enter this key in Settings → Admin Plan → License Key section",
"canCloseWindow": "You can now close this window.",
"licenseKeyProcessing": "License Key Processing",
"licenseDelayedMessage": "Your license key is being generated. Please check your email shortly or contact support."
},
"firstLogin": {
"title": "First Time Login",

View File

@ -5,7 +5,7 @@ import { loadStripe } from '@stripe/stripe-js';
import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js';
import licenseService, { PlanTierGroup } from '@app/services/licenseService';
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
import { pollLicenseKeyWithBackoff, activateLicenseKey } from '@app/utils/licenseCheckoutUtils';
import { pollLicenseKeyWithBackoff, activateLicenseKey, resyncExistingLicense } from '@app/utils/licenseCheckoutUtils';
// Validate Stripe key (static validation, no dynamic imports)
const STRIPE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
@ -168,11 +168,11 @@ const StripeCheckout: React.FC<StripeCheckoutProps> = ({
// Check if this is an upgrade (existing license key) or new plan
if (currentLicenseKey) {
// UPGRADE FLOW: Force license re-verification by saving existing key
console.log('Upgrade detected - syncing existing license key');
// UPGRADE FLOW: Resync existing license with Keygen
console.log('Upgrade detected - resyncing existing license with Keygen');
setPollingStatus('polling');
const activation = await activateLicenseKey(currentLicenseKey, {
const activation = await resyncExistingLicense({
isMounted: () => true, // Modal is open, no need to check
onActivated: onLicenseActivated,
});

View File

@ -15,7 +15,6 @@ 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';
import { pollLicenseKeyWithBackoff, activateLicenseKey } from '@app/utils/licenseCheckoutUtils';
interface PremiumSettingsData {
key?: string;
@ -62,107 +61,6 @@ const AdminPlanSection: React.FC = () => {
}
};
// Handle return from hosted Stripe checkout
const handleCheckoutReturn = async () => {
const urlParams = new URLSearchParams(window.location.search);
const paymentStatus = urlParams.get('payment_status');
const sessionId = urlParams.get('session_id');
if (paymentStatus === 'success' && sessionId) {
console.log('Payment successful via hosted checkout:', sessionId);
// Clear URL parameters
window.history.replaceState({}, '', window.location.pathname);
// Check if this is an upgrade or new subscription
if (currentLicenseInfo?.licenseKey) {
// UPGRADE: Sync existing license key
console.log('Upgrade detected - syncing existing license');
const activation = await activateLicenseKey(currentLicenseInfo.licenseKey, {
onActivated: fetchLicenseInfo,
});
if (activation.success) {
alert({
message: t('payment.upgradeSuccess', 'Your subscription has been upgraded successfully!'),
color: 'green',
});
} else {
console.error('Failed to sync license after upgrade:', activation.error);
alert({
message: t('payment.syncError', 'Payment successful but license sync failed. Please contact support.'),
color: 'red',
});
}
} else {
// NEW SUBSCRIPTION: Poll for license key
console.log('New subscription - polling for license key');
alert({
message: t('payment.paymentSuccess', 'Payment successful! Retrieving your license key...'),
color: 'green',
});
try {
const installationId = await licenseService.getInstallationId();
console.log('Polling for license key with installation ID:', installationId);
// Use shared polling utility
const result = await pollLicenseKeyWithBackoff(installationId);
if (result.success && result.licenseKey) {
// Activate the license key
const activation = await activateLicenseKey(result.licenseKey, {
onActivated: fetchLicenseInfo,
});
if (activation.success) {
console.log(`License key activated: ${activation.licenseType}`);
alert({
message: t('payment.licenseActivated', 'License key activated successfully!'),
color: 'green',
});
} else {
console.error('Failed to save license key:', activation.error);
alert({
message: t('payment.licenseSaveError', 'Failed to save license key. Please contact support.'),
color: 'red',
});
}
} else if (result.timedOut) {
console.warn('License key polling timed out');
alert({
message: t('payment.licenseDelayed', 'License key is being generated. Please check back shortly or contact support.'),
color: 'yellow',
});
} else {
console.error('License key polling failed:', result.error);
alert({
message: t('payment.licensePollingError', 'Failed to retrieve license key. Please check your email or contact support.'),
color: 'red',
});
}
} catch (error) {
console.error('Failed to poll for license key:', error);
alert({
message: t('payment.licenseRetrievalError', 'Failed to retrieve license key. Please check your email or contact support.'),
color: 'red',
});
}
}
} else if (paymentStatus === 'canceled') {
console.log('Payment canceled by user');
// Clear URL parameters
window.history.replaceState({}, '', window.location.pathname);
alert({
message: t('payment.paymentCanceled', 'Payment was canceled.'),
color: 'yellow',
});
}
};
// Check if Stripe is configured
const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
if (!stripeKey || error) {
@ -170,9 +68,6 @@ const AdminPlanSection: React.FC = () => {
}
fetchLicenseInfo();
// Handle checkout return after license info is loaded
handleCheckoutReturn();
// Fetch premium settings
fetchPremiumSettings();
}, [error, config]);

View File

@ -1,8 +1,11 @@
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { usePlans } from '@app/hooks/usePlans';
import licenseService, { PlanTierGroup, LicenseInfo, mapLicenseToTier } from '@app/services/licenseService';
import StripeCheckout from '@app/components/shared/StripeCheckout';
import { userManagementService } from '@app/services/userManagementService';
import { alert } from '@app/components/toast';
import { pollLicenseKeyWithBackoff, activateLicenseKey, resyncExistingLicense } from '@app/utils/licenseCheckoutUtils';
export interface CheckoutOptions {
minimumSeats?: number; // Override calculated seats for enterprise
@ -32,6 +35,7 @@ export const CheckoutProvider: React.FC<CheckoutProviderProps> = ({
children,
defaultCurrency = 'gbp'
}) => {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [selectedPlanGroup, setSelectedPlanGroup] = useState<PlanTierGroup | null>(null);
@ -42,6 +46,117 @@ export const CheckoutProvider: React.FC<CheckoutProviderProps> = ({
// Load plans with current currency
const { plans, refetch: refetchPlans } = usePlans(currentCurrency);
// Handle return from hosted Stripe checkout
useEffect(() => {
const handleCheckoutReturn = async () => {
const urlParams = new URLSearchParams(window.location.search);
const paymentStatus = urlParams.get('payment_status');
const sessionId = urlParams.get('session_id');
if (paymentStatus === 'success' && sessionId) {
console.log('Payment successful via hosted checkout:', sessionId);
// Clear URL parameters
window.history.replaceState({}, '', window.location.pathname);
// Fetch current license info to determine upgrade vs new
let licenseInfo: LicenseInfo | null = null;
try {
licenseInfo = await licenseService.getLicenseInfo();
} catch (err) {
console.warn('Could not fetch license info:', err);
}
// Check if this is an upgrade or new subscription
if (licenseInfo?.licenseKey) {
// UPGRADE: Resync existing license with Keygen
console.log('Upgrade detected - resyncing existing license');
const activation = await resyncExistingLicense();
if (activation.success) {
alert({
alertType: 'success',
title: t('payment.upgradeSuccess'),
});
refetchPlans(); // Refresh plans to show updated subscription
} else {
console.error('Failed to sync license after upgrade:', activation.error);
alert({
alertType: 'error',
title: t('payment.syncError'),
});
}
} else {
// NEW SUBSCRIPTION: Poll for license key
console.log('New subscription - polling for license key');
alert({
alertType: 'success',
title: t('payment.paymentSuccess'),
});
try {
const installationId = await licenseService.getInstallationId();
console.log('Polling for license key with installation ID:', installationId);
// Use shared polling utility
const result = await pollLicenseKeyWithBackoff(installationId);
if (result.success && result.licenseKey) {
// Activate the license key
const activation = await activateLicenseKey(result.licenseKey);
if (activation.success) {
console.log(`License key activated: ${activation.licenseType}`);
alert({
alertType: 'success',
title: t('payment.licenseActivated'),
});
refetchPlans(); // Refresh plans to show updated subscription
} else {
console.error('Failed to save license key:', activation.error);
alert({
alertType: 'error',
title: t('payment.licenseSaveError'),
});
}
} else if (result.timedOut) {
console.warn('License key polling timed out');
alert({
alertType: 'warning',
title: t('payment.licenseDelayed'),
});
} else {
console.error('License key polling failed:', result.error);
alert({
alertType: 'error',
title: t('payment.licensePollingError'),
});
}
} catch (error) {
console.error('Failed to poll for license key:', error);
alert({
alertType: 'error',
title: t('payment.licenseRetrievalError'),
});
}
}
} else if (paymentStatus === 'canceled') {
console.log('Payment canceled by user');
// Clear URL parameters
window.history.replaceState({}, '', window.location.pathname);
alert({
alertType: 'warning',
title: t('payment.paymentCanceled'),
});
}
};
handleCheckoutReturn();
}, [t, refetchPlans]);
const openCheckout = useCallback(
async (tier: 'server' | 'enterprise', options: CheckoutOptions = {}) => {
try {

View File

@ -470,6 +470,20 @@ const licenseService = {
throw error;
}
},
/**
* Resync the current license with Keygen
* Re-validates the existing license key and updates local settings
*/
async resyncLicense(): Promise<LicenseSaveResponse> {
try {
const response = await apiClient.post('/api/v1/admin/license/resync');
return response.data;
} catch (error) {
console.error('Error resyncing license:', error);
throw error;
}
},
};
/**

View File

@ -134,7 +134,7 @@ export interface LicenseActivationResult {
/**
* Activate a license key by saving it to the backend and fetching updated info
* Consolidates activation logic used by both embedded and hosted checkout
* Used for NEW subscriptions where we have a new license key to save
*/
export async function activateLicenseKey(
licenseKey: string,
@ -197,3 +197,69 @@ export async function activateLicenseKey(
};
}
}
/**
* Resync existing license with Keygen
* Used for UPGRADES where we already have a license key configured
* Calls the dedicated resync endpoint instead of re-saving the same key
*/
export async function resyncExistingLicense(
options: {
/** Check if component is still mounted */
isMounted?: () => boolean;
/** Callback when license is resynced with updated info */
onActivated?: (licenseInfo: LicenseInfo) => void;
} = {}
): Promise<LicenseActivationResult> {
const { isMounted = () => true, onActivated } = options;
try {
console.log('Resyncing existing license with Keygen...');
const resyncResponse = await licenseService.resyncLicense();
if (!isMounted()) {
return { success: false, error: 'Component unmounted' };
}
if (resyncResponse.success) {
console.log(`License resynced: ${resyncResponse.licenseType}`);
// Fetch updated license info
try {
const licenseInfo = await licenseService.getLicenseInfo();
if (!isMounted()) {
return { success: false, error: 'Component unmounted' };
}
onActivated?.(licenseInfo);
return {
success: true,
licenseType: resyncResponse.licenseType,
licenseInfo,
};
} catch (infoError) {
console.error('Error fetching license info after resync:', infoError);
// Still return success since resync succeeded
return {
success: true,
licenseType: resyncResponse.licenseType,
error: 'Failed to fetch updated license info',
};
}
} else {
console.error('Failed to resync license:', resyncResponse.error);
return {
success: false,
error: resyncResponse.error || 'Failed to resync license',
};
}
} catch (error) {
console.error('Error resyncing license:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Resync failed',
};
}
}

View File

@ -10,7 +10,7 @@
export function isSecureContext(): boolean {
// Allow localhost for development (works with both HTTP and HTTPS)
if (typeof window !== 'undefined') {
const hostname = window.location.hostname;
// const hostname = window.location.hostname;
const protocol = window.location.protocol;
// Localhost is considered secure for development