Skip to content

Commit d5b43a0

Browse files
Refactor license validation logic to use getValidatedLicenseData and update tests for consistency
1 parent 895d92a commit d5b43a0

File tree

3 files changed

+187
-231
lines changed

3 files changed

+187
-231
lines changed

react_on_rails_pro/packages/node-renderer/src/master.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import log from './shared/log';
77
import { buildConfig, Config, logSanitizedConfig } from './shared/configBuilder';
88
import restartWorkers from './master/restartWorkers';
99
import * as errorReporter from './shared/errorReporter';
10-
import { validateLicense } from './shared/licenseValidator';
10+
import { getValidatedLicenseData } from './shared/licenseValidator';
1111

1212
const MILLISECONDS_IN_MINUTE = 60000;
1313

1414
export = function masterRun(runningConfig?: Partial<Config>) {
1515
// Validate license before starting - required in all environments
1616
log.info('[React on Rails Pro] Validating license...');
17-
validateLicense();
17+
getValidatedLicenseData();
1818
log.info('[React on Rails Pro] License validation successful');
1919

2020
// Store config in app state. From now it can be loaded by any module using getConfig():

react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts

Lines changed: 105 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,13 @@ interface LicenseData {
1818
[key: string]: unknown;
1919
}
2020

21-
// Module-level state for caching
22-
let cachedValid: boolean | undefined;
23-
let cachedLicenseData: LicenseData | undefined;
24-
let cachedValidationError: string | undefined;
25-
2621
// Grace period: 1 month (in seconds)
2722
const GRACE_PERIOD_SECONDS = 30 * 24 * 60 * 60;
2823

24+
// Module-level state for caching
25+
let cachedLicenseData: LicenseData | undefined;
26+
let cachedGraceDaysRemaining: number | undefined;
27+
2928
/**
3029
* Handles invalid license by logging error and exiting.
3130
* @private
@@ -38,29 +37,29 @@ function handleInvalidLicense(message: string): never {
3837
}
3938

4039
/**
41-
* Checks if the current environment is production.
40+
* Checks if running in production environment.
4241
* @private
4342
*/
4443
function isProduction(): boolean {
4544
return process.env.NODE_ENV === 'production';
4645
}
4746

4847
/**
49-
* Checks if the license is within the grace period.
48+
* Checks if current time is within grace period after expiration.
5049
* @private
5150
*/
5251
function isWithinGracePeriod(expTime: number): boolean {
53-
return Date.now() / 1000 <= expTime + GRACE_PERIOD_SECONDS;
52+
return Math.floor(Date.now() / 1000) <= expTime + GRACE_PERIOD_SECONDS;
5453
}
5554

5655
/**
5756
* Calculates remaining grace period days.
5857
* @private
5958
*/
60-
function graceDaysRemaining(expTime: number): number {
59+
function calculateGraceDaysRemaining(expTime: number): number {
6160
const graceEnd = expTime + GRACE_PERIOD_SECONDS;
62-
const secondsRemaining = graceEnd - Date.now() / 1000;
63-
return Math.floor(secondsRemaining / (24 * 60 * 60));
61+
const secondsRemaining = graceEnd - Math.floor(Date.now() / 1000);
62+
return secondsRemaining <= 0 ? 0 : Math.floor(secondsRemaining / (24 * 60 * 60));
6463
}
6564

6665
/**
@@ -83,30 +82,29 @@ function logLicenseInfo(license: LicenseData): void {
8382
* @private
8483
*/
8584
// eslint-disable-next-line consistent-return
86-
function loadLicenseString(): string | never {
85+
function loadLicenseString(): string {
8786
// First try environment variable
8887
const envLicense = process.env.REACT_ON_RAILS_PRO_LICENSE;
8988
if (envLicense) {
9089
return envLicense;
9190
}
9291

9392
// Then try config file (relative to project root)
94-
let configPath;
9593
try {
96-
configPath = path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key');
94+
const configPath = path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key');
9795
if (fs.existsSync(configPath)) {
9896
return fs.readFileSync(configPath, 'utf8').trim();
9997
}
10098
} catch (error) {
10199
console.error(`[React on Rails Pro] Error reading license file: ${(error as Error).message}`);
102100
}
103101

104-
cachedValidationError =
102+
const errorMsg =
105103
'No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable ' +
106-
`or create ${configPath ?? 'config/react_on_rails_pro_license.key'} file. ` +
104+
'or create config/react_on_rails_pro_license.key file. ' +
107105
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
108106

109-
handleInvalidLicense(cachedValidationError);
107+
handleInvalidLicense(errorMsg);
110108
}
111109

112110
/**
@@ -126,119 +124,136 @@ function loadAndDecodeLicense(): LicenseData {
126124
ignoreExpiration: true,
127125
}) as LicenseData;
128126

129-
cachedLicenseData = decoded;
130127
return decoded;
131128
}
132129

133130
/**
134-
* Performs the actual license validation logic.
131+
* Validates the license data and throws if invalid.
132+
* Logs info/errors and handles grace period logic.
133+
*
134+
* @param license - The decoded license data
135+
* @returns Grace days remaining if in grace period, undefined otherwise
136+
* @throws Never returns - exits process if license is invalid
135137
* @private
136138
*/
137-
// eslint-disable-next-line consistent-return
138-
function performValidation(): boolean | never {
139-
try {
140-
const license = loadAndDecodeLicense();
139+
function validateLicenseData(license: LicenseData): number | undefined {
140+
// Check that exp field exists
141+
if (!license.exp) {
142+
const error =
143+
'License is missing required expiration field. ' +
144+
'Your license may be from an older version. ' +
145+
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
146+
handleInvalidLicense(error);
147+
}
141148

142-
// Check that exp field exists
143-
if (!license.exp) {
144-
cachedValidationError =
145-
'License is missing required expiration field. ' +
146-
'Your license may be from an older version. ' +
147-
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
148-
handleInvalidLicense(cachedValidationError);
149-
}
149+
// Check expiry with grace period for production
150+
const currentTime = Math.floor(Date.now() / 1000);
151+
const expTime = license.exp;
152+
let graceDays: number | undefined;
153+
154+
if (currentTime > expTime) {
155+
const daysExpired = Math.floor((currentTime - expTime) / (24 * 60 * 60));
150156

151-
// Check expiry with grace period for production
152-
// Date.now() returns milliseconds, but JWT exp is in Unix seconds, so divide by 1000
153-
const currentTime = Date.now() / 1000;
154-
const expTime = license.exp;
155-
156-
if (currentTime > expTime) {
157-
const daysExpired = Math.floor((currentTime - expTime) / (24 * 60 * 60));
158-
159-
cachedValidationError =
160-
`License has expired ${daysExpired} day(s) ago. ` +
161-
'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
162-
'or upgrade to a paid license for production use.';
163-
164-
// In production, allow a grace period of 1 month with error logging
165-
if (isProduction() && isWithinGracePeriod(expTime)) {
166-
const graceDays = graceDaysRemaining(expTime);
167-
console.error(
168-
`[React on Rails Pro] WARNING: ${cachedValidationError} ` +
169-
`Grace period: ${graceDays} day(s) remaining. ` +
170-
'Application will fail to start after grace period expires.',
171-
);
172-
} else {
173-
handleInvalidLicense(cachedValidationError);
174-
}
157+
const error =
158+
`License has expired ${daysExpired} day(s) ago. ` +
159+
'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
160+
'or upgrade to a paid license for production use.';
161+
162+
// In production, allow a grace period of 1 month with error logging
163+
if (isProduction() && isWithinGracePeriod(expTime)) {
164+
// Calculate grace days once here
165+
graceDays = calculateGraceDaysRemaining(expTime);
166+
console.error(
167+
`[React on Rails Pro] WARNING: ${error} ` +
168+
`Grace period: ${graceDays} day(s) remaining. ` +
169+
'Application will fail to start after grace period expires.',
170+
);
171+
} else {
172+
handleInvalidLicense(error);
175173
}
174+
}
175+
176+
// Log license type if present (for analytics)
177+
logLicenseInfo(license);
176178

177-
// Log license type if present (for analytics)
178-
logLicenseInfo(license);
179+
// Return grace days (undefined if not in grace period)
180+
return graceDays;
181+
}
179182

180-
return true;
183+
/**
184+
* Validates the license and returns the license data.
185+
* Caches the result after first validation.
186+
*
187+
* @returns The validated license data
188+
* @throws Exits process if license is invalid
189+
*/
190+
// eslint-disable-next-line consistent-return
191+
export function getValidatedLicenseData(): LicenseData {
192+
if (cachedLicenseData !== undefined) {
193+
return cachedLicenseData;
194+
}
195+
196+
try {
197+
// Load and decode license (but don't cache yet)
198+
const licenseData = loadAndDecodeLicense();
199+
200+
// Validate the license (raises if invalid, returns grace_days)
201+
const graceDays = validateLicenseData(licenseData);
202+
203+
// Validation passed - now cache both data and grace days
204+
cachedLicenseData = licenseData;
205+
cachedGraceDaysRemaining = graceDays;
206+
207+
return cachedLicenseData;
181208
} catch (error: unknown) {
182209
if (error instanceof Error && error.name === 'JsonWebTokenError') {
183-
cachedValidationError =
210+
const errorMsg =
184211
`Invalid license signature: ${error.message}. ` +
185212
'Your license file may be corrupted. ' +
186213
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
214+
handleInvalidLicense(errorMsg);
187215
} else if (error instanceof Error) {
188-
cachedValidationError =
216+
const errorMsg =
189217
`License validation error: ${error.message}. ` +
190218
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
219+
handleInvalidLicense(errorMsg);
191220
} else {
192-
cachedValidationError =
221+
const errorMsg =
193222
'License validation error: Unknown error. ' +
194223
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
224+
handleInvalidLicense(errorMsg);
195225
}
196-
handleInvalidLicense(cachedValidationError);
197226
}
198227
}
199228

200229
/**
201-
* Validates the license and exits the process if invalid.
202-
* Caches the result after first validation.
230+
* Checks if the current license is an evaluation/free license.
203231
*
204-
* @returns true if license is valid
205-
* @throws Exits process if license is invalid
232+
* @returns true if plan is not "paid"
206233
*/
207-
export function validateLicense(): boolean {
208-
if (cachedValid !== undefined) {
209-
return cachedValid;
210-
}
211-
212-
cachedValid = performValidation();
213-
return cachedValid;
234+
export function isEvaluation(): boolean {
235+
const data = getValidatedLicenseData();
236+
const plan = String(data.plan || '');
237+
return plan !== 'paid' && !plan.startsWith('paid_');
214238
}
215239

216240
/**
217-
* Gets the decoded license data.
241+
* Returns remaining grace period days if license is expired but in grace period.
218242
*
219-
* @returns Decoded license data or undefined if no license
243+
* @returns Number of days remaining, or undefined if not in grace period
220244
*/
221-
export function getLicenseData(): LicenseData | undefined {
222-
if (!cachedLicenseData) {
223-
loadAndDecodeLicense();
224-
}
225-
return cachedLicenseData;
226-
}
245+
export function getGraceDaysRemaining(): number | undefined {
246+
// Ensure license is validated and cached
247+
getValidatedLicenseData();
227248

228-
/**
229-
* Gets the validation error message if validation failed.
230-
*
231-
* @returns Error message or undefined
232-
*/
233-
export function getValidationError(): string | undefined {
234-
return cachedValidationError;
249+
// Return cached grace days (undefined if not in grace period)
250+
return cachedGraceDaysRemaining;
235251
}
236252

237253
/**
238254
* Resets all cached validation state (primarily for testing).
239255
*/
240256
export function reset(): void {
241-
cachedValid = undefined;
242257
cachedLicenseData = undefined;
243-
cachedValidationError = undefined;
258+
cachedGraceDaysRemaining = undefined;
244259
}

0 commit comments

Comments
 (0)