Skip to content

Commit fca96e4

Browse files
Refactor Node.js license validator from singleton class to functional pattern
1 parent ab9801c commit fca96e4

File tree

3 files changed

+198
-186
lines changed

3 files changed

+198
-186
lines changed

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,24 @@ 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 { isLicenseValid, getLicenseValidationError } from './shared/licenseValidator';
10+
import { validateLicense, getValidationError } 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...');
1717

18-
if (!isLicenseValid()) {
19-
const error = getLicenseValidationError() || 'Invalid license';
20-
log.error(`[React on Rails Pro] ${error}`);
18+
if (validateLicense()) {
19+
log.info('[React on Rails Pro] License validation successful');
20+
} else {
2121
// License validation already calls process.exit(1) in handleInvalidLicense
2222
// But we add this for safety in case the validator changes
23+
const error = getValidationError() || 'Invalid license';
24+
log.error(`[React on Rails Pro] ${error}`);
2325
process.exit(1);
2426
}
2527

26-
log.info('[React on Rails Pro] License validation successful');
27-
2828
// Store config in app state. From now it can be loaded by any module using getConfig():
2929
const config = buildConfig(runningConfig);
3030
const { workersCount, allWorkersRestartInterval, delayBetweenIndividualWorkerRestarts } = config;

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

Lines changed: 142 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -10,167 +10,171 @@ interface LicenseData {
1010
[key: string]: any;
1111
}
1212

13-
class LicenseValidator {
14-
private static instance: LicenseValidator;
15-
private valid?: boolean;
16-
private licenseData?: LicenseData;
17-
private validationError?: string;
18-
19-
private constructor() {}
20-
21-
public static getInstance(): LicenseValidator {
22-
if (!LicenseValidator.instance) {
23-
LicenseValidator.instance = new LicenseValidator();
24-
}
25-
return LicenseValidator.instance;
13+
// Module-level state for caching
14+
let cachedValid: boolean | undefined;
15+
let cachedLicenseData: LicenseData | undefined;
16+
let cachedValidationError: string | undefined;
17+
18+
/**
19+
* Validates the license and raises an error if invalid.
20+
* Caches the result after first validation.
21+
*
22+
* @returns true if license is valid
23+
* @throws Exits process if license is invalid
24+
*/
25+
export function validateLicense(): boolean {
26+
if (cachedValid !== undefined) {
27+
return cachedValid;
2628
}
2729

28-
public isValid(): boolean {
29-
if (this.valid !== undefined) {
30-
return this.valid;
31-
}
32-
33-
this.valid = this.validateLicense();
34-
return this.valid;
35-
}
30+
cachedValid = performValidation();
31+
return cachedValid;
32+
}
3633

37-
public getLicenseData(): LicenseData | undefined {
38-
if (!this.licenseData) {
39-
this.loadAndDecodeLicense();
40-
}
41-
return this.licenseData;
34+
/**
35+
* Gets the decoded license data.
36+
*
37+
* @returns Decoded license data or undefined if no license
38+
*/
39+
export function getLicenseData(): LicenseData | undefined {
40+
if (!cachedLicenseData) {
41+
loadAndDecodeLicense();
4242
}
43+
return cachedLicenseData;
44+
}
4345

44-
public getValidationError(): string | undefined {
45-
return this.validationError;
46-
}
46+
/**
47+
* Gets the validation error message if validation failed.
48+
*
49+
* @returns Error message or undefined
50+
*/
51+
export function getValidationError(): string | undefined {
52+
return cachedValidationError;
53+
}
4754

48-
public reset(): void {
49-
this.valid = undefined;
50-
this.licenseData = undefined;
51-
this.validationError = undefined;
52-
}
55+
/**
56+
* Resets all cached validation state (primarily for testing).
57+
*/
58+
export function reset(): void {
59+
cachedValid = undefined;
60+
cachedLicenseData = undefined;
61+
cachedValidationError = undefined;
62+
}
5363

54-
private validateLicense(): boolean {
55-
try {
56-
const license = this.loadAndDecodeLicense();
57-
if (!license) {
58-
return false;
59-
}
60-
61-
// Check that exp field exists
62-
if (!license.exp) {
63-
this.validationError = 'License is missing required expiration field. ' +
64-
'Your license may be from an older version. ' +
65-
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
66-
this.handleInvalidLicense(this.validationError);
67-
return false;
68-
}
69-
70-
// Check expiry
71-
if (Date.now() / 1000 > license.exp) {
72-
this.validationError = 'License has expired. ' +
73-
'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
74-
'or upgrade to a paid license for production use.';
75-
this.handleInvalidLicense(this.validationError);
76-
return false;
77-
}
78-
79-
// Log license type if present (for analytics)
80-
this.logLicenseInfo(license);
81-
82-
return true;
83-
} catch (error: any) {
84-
if (error.name === 'JsonWebTokenError') {
85-
this.validationError = `Invalid license signature: ${error.message}. ` +
86-
'Your license file may be corrupted. ' +
87-
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
88-
} else {
89-
this.validationError = `License validation error: ${error.message}. ` +
90-
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
91-
}
92-
this.handleInvalidLicense(this.validationError);
93-
return false;
64+
/**
65+
* Performs the actual license validation logic.
66+
* @private
67+
*/
68+
function performValidation(): boolean {
69+
try {
70+
const license = loadAndDecodeLicense();
71+
72+
// Check that exp field exists
73+
if (!license.exp) {
74+
cachedValidationError =
75+
'License is missing required expiration field. ' +
76+
'Your license may be from an older version. ' +
77+
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
78+
handleInvalidLicense(cachedValidationError);
9479
}
95-
}
9680

97-
private loadAndDecodeLicense(): LicenseData | undefined {
98-
const licenseString = this.loadLicenseString();
99-
if (!licenseString) {
100-
return undefined;
81+
// Check expiry
82+
if (Date.now() / 1000 > license.exp) {
83+
cachedValidationError =
84+
'License has expired. ' +
85+
'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
86+
'or upgrade to a paid license for production use.';
87+
handleInvalidLicense(cachedValidationError);
10188
}
10289

103-
try {
104-
const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
105-
algorithms: ['RS256'],
106-
// Disable automatic expiration verification so we can handle it manually with custom logic
107-
ignoreExpiration: true
108-
}) as LicenseData;
109-
110-
this.licenseData = decoded;
111-
return decoded;
112-
} catch (error) {
113-
throw error;
90+
// Log license type if present (for analytics)
91+
logLicenseInfo(license);
92+
93+
return true;
94+
} catch (error: any) {
95+
if (error.name === 'JsonWebTokenError') {
96+
cachedValidationError =
97+
`Invalid license signature: ${error.message}. ` +
98+
'Your license file may be corrupted. ' +
99+
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
100+
} else {
101+
cachedValidationError =
102+
`License validation error: ${error.message}. ` +
103+
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
114104
}
105+
handleInvalidLicense(cachedValidationError);
115106
}
107+
}
116108

117-
private loadLicenseString(): string | undefined {
118-
// First try environment variable
119-
const envLicense = process.env.REACT_ON_RAILS_PRO_LICENSE;
120-
if (envLicense) {
121-
return envLicense;
122-
}
123-
124-
// Then try config file (relative to project root)
125-
try {
126-
const configPath = path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key');
127-
if (fs.existsSync(configPath)) {
128-
return fs.readFileSync(configPath, 'utf8').trim();
129-
}
130-
} catch (error) {
131-
// File doesn't exist or can't be read
132-
}
133-
134-
this.validationError =
135-
'No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable ' +
136-
'or create config/react_on_rails_pro_license.key file. ' +
137-
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
138-
139-
this.handleInvalidLicense(this.validationError);
140-
141-
return undefined;
142-
}
109+
/**
110+
* Loads and decodes the license from environment or file.
111+
* @private
112+
*/
113+
function loadAndDecodeLicense(): LicenseData {
114+
const licenseString = loadLicenseString();
115+
116+
const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
117+
algorithms: ['RS256'],
118+
// Disable automatic expiration verification so we can handle it manually with custom logic
119+
ignoreExpiration: true,
120+
}) as LicenseData;
121+
122+
cachedLicenseData = decoded;
123+
return decoded;
124+
}
143125

144-
private handleInvalidLicense(message: string): void {
145-
const fullMessage = `[React on Rails Pro] ${message}`;
146-
console.error(fullMessage);
147-
// Validation errors should prevent the application from starting
148-
process.exit(1);
126+
/**
127+
* Loads the license string from environment variable or config file.
128+
* @private
129+
*/
130+
function loadLicenseString(): string {
131+
// First try environment variable
132+
const envLicense = process.env.REACT_ON_RAILS_PRO_LICENSE;
133+
if (envLicense) {
134+
return envLicense;
149135
}
150136

151-
private logLicenseInfo(license: LicenseData): void {
152-
const plan = (license as any).plan;
153-
const issuedBy = (license as any).issued_by;
154-
155-
if (plan) {
156-
console.log(`[React on Rails Pro] License plan: ${plan}`);
157-
}
158-
if (issuedBy) {
159-
console.log(`[React on Rails Pro] Issued by: ${issuedBy}`);
137+
// Then try config file (relative to project root)
138+
try {
139+
const configPath = path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key');
140+
if (fs.existsSync(configPath)) {
141+
return fs.readFileSync(configPath, 'utf8').trim();
160142
}
143+
} catch (error) {
144+
// File doesn't exist or can't be read
161145
}
162-
}
163146

164-
export const licenseValidator = LicenseValidator.getInstance();
147+
cachedValidationError =
148+
'No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable ' +
149+
'or create config/react_on_rails_pro_license.key file. ' +
150+
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
165151

166-
export function isLicenseValid(): boolean {
167-
return licenseValidator.isValid();
152+
handleInvalidLicense(cachedValidationError);
168153
}
169154

170-
export function getLicenseData(): LicenseData | undefined {
171-
return licenseValidator.getLicenseData();
155+
/**
156+
* Handles invalid license by logging error and exiting.
157+
* @private
158+
*/
159+
function handleInvalidLicense(message: string): never {
160+
const fullMessage = `[React on Rails Pro] ${message}`;
161+
console.error(fullMessage);
162+
// Validation errors should prevent the application from starting
163+
process.exit(1);
172164
}
173165

174-
export function getLicenseValidationError(): string | undefined {
175-
return licenseValidator.getValidationError();
166+
/**
167+
* Logs license information for analytics.
168+
* @private
169+
*/
170+
function logLicenseInfo(license: LicenseData): void {
171+
const plan = (license as any).plan;
172+
const issuedBy = (license as any).issued_by;
173+
174+
if (plan) {
175+
console.log(`[React on Rails Pro] License plan: ${plan}`);
176+
}
177+
if (issuedBy) {
178+
console.log(`[React on Rails Pro] Issued by: ${issuedBy}`);
179+
}
176180
}

0 commit comments

Comments
 (0)