@mostajs/licensing-ui-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/llms.txt +43 -0
- package/package.json +38 -0
- package/src/index.ts +67 -0
- package/src/lib/license-service.ts +360 -0
- package/src/lib/licenseClient.ts +76 -0
- package/src/react/EnhancedLicenseGate.tsx +488 -0
- package/src/react/LicenseActivationForm.tsx +365 -0
- package/src/react/LicenseConfigForm.tsx +243 -0
- package/src/react/LicenseGate.tsx +504 -0
- package/src/react/LicenseProvider.tsx +36 -0
- package/src/react/PendingLicenseActivation.tsx +270 -0
- package/src/react/SimpleLicenseActivation.tsx +273 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @mostajs/licensing-ui-react
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
UI de **gestion de licences** (gate d'accès, activation, configuration) — **extraite de
|
|
6
|
+
`SolutionCh/MostaGare`** et **composant `@mostajs/licensing`** (DEVRULES §10 : aucune logique de
|
|
7
|
+
licence réécrite). **Config pilotée par `.env`.**
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createLicenseUiFromEnv, LicenseGate, LicenseActivationForm } from '@mostajs/licensing-ui-react';
|
|
11
|
+
|
|
12
|
+
// Contrôleur configuré depuis .env (LICENSE_SERVER_URL / LICENSE_PROJECT / LICENSE_PUBLIC_KEY)
|
|
13
|
+
const lic = createLicenseUiFromEnv(process.env);
|
|
14
|
+
lic.check(signedLicense); // vérif RSA + projet + expiration
|
|
15
|
+
lic.requestCode({ email }); // demande un code d'activation
|
|
16
|
+
lic.activate({ activationCode, machineId }); // download + verify + notify
|
|
17
|
+
|
|
18
|
+
// Composants React (Next/React) — gate d'accès
|
|
19
|
+
<LicenseGate project="salsabil"><App/></LicenseGate>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Membres / config
|
|
23
|
+
Stack : `mosta-licensing-stack`. Pair logique : `@mostajs/licensing`.
|
|
24
|
+
Variables `.env` : `LICENSE_SERVER_URL` (défaut `https://licences.amia.fr`), `LICENSE_PROJECT`,
|
|
25
|
+
`LICENSE_PUBLIC_KEY` (PEM base64).
|
|
26
|
+
|
|
27
|
+
## Provenance
|
|
28
|
+
Composants extraits : `LicenseGate`, `EnhancedLicenseGate`, `LicenseActivationForm`,
|
|
29
|
+
`SimpleLicenseActivation`, `PendingLicenseActivation`, `LicenseConfigForm` + `lib/license-service`,
|
|
30
|
+
`lib/licenseClient`. **À généraliser** : remplacer les URLs/projets codés en dur par
|
|
31
|
+
`createLicenseUiFromEnv`. Fiche LLM : `llms.txt`.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @mostajs/licensing-ui-react — fiche LLM
|
|
2
|
+
> UI de licence (gate d'accès, activation, configuration) — extraite de MostaGare, compose @mostajs/licensing.
|
|
3
|
+
|
|
4
|
+
- Version: 0.1.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
- Stack: mosta-licensing-stack · Pair logique: @mostajs/licensing
|
|
6
|
+
|
|
7
|
+
## RÔLE
|
|
8
|
+
Couche de PRÉSENTATION des licences. Composants React EXTRAITS de SolutionCh/MostaGare
|
|
9
|
+
(LicenseGate, EnhancedLicenseGate, LicenseActivationForm, SimpleLicenseActivation,
|
|
10
|
+
PendingLicenseActivation, LicenseConfigForm) + un contrôleur HEADLESS qui COMPOSE
|
|
11
|
+
@mostajs/licensing (DEVRULES §10 : aucune logique de licence réécrite).
|
|
12
|
+
|
|
13
|
+
## EXPORTS
|
|
14
|
+
- LicenseProvider, useLicenseUi ← contexte alimenté par createLicenseUiFromEnv (config .env)
|
|
15
|
+
- Composants React (GÉNÉRALISÉS : consomment useLicenseUi, plus d'URL/projet en dur) :
|
|
16
|
+
LicenseGate, EnhancedLicenseGate, LicenseActivationForm, SimpleLicenseActivation,
|
|
17
|
+
PendingLicenseActivation, LicenseConfigForm (./react/*)
|
|
18
|
+
- createLicenseUi(opts) -> { client, check, requestCode, activate }
|
|
19
|
+
- createLicenseUiFromEnv(env) / licenseUiConfigFromEnv(env) ← CONFIG PILOTÉE PAR .env
|
|
20
|
+
|
|
21
|
+
## USAGE REACT
|
|
22
|
+
Entourer l'app de <LicenseProvider env={{ LICENSE_SERVER_URL, LICENSE_PROJECT, LICENSE_PUBLIC_KEY }}>
|
|
23
|
+
puis utiliser <LicenseGate>/<LicenseActivationForm> (qui lisent serverUrl/project via useLicenseUi()).
|
|
24
|
+
|
|
25
|
+
## CONFIG .env
|
|
26
|
+
- LICENSE_SERVER_URL (défaut https://licences.amia.fr)
|
|
27
|
+
- LICENSE_PROJECT (ex. salsabil)
|
|
28
|
+
- LICENSE_PUBLIC_KEY (clé publique PEM, base64)
|
|
29
|
+
|
|
30
|
+
## API (contrôleur headless, compose @mostajs/licensing)
|
|
31
|
+
- check(signed) -> { valid, status, reason?, expiresAt } (checkLicense)
|
|
32
|
+
- requestCode({ email, company?, message? }) (POST /api/request-activation-code)
|
|
33
|
+
- activate({ activationCode, machineId, machineName? }) (download + verify + notify)
|
|
34
|
+
- client : le createLicenseClient brut de @mostajs/licensing
|
|
35
|
+
|
|
36
|
+
## DÉPEND DE (peer)
|
|
37
|
+
- @mostajs/licensing (logique : checkLicense, createLicenseClient), react, lucide-react
|
|
38
|
+
|
|
39
|
+
## NOTE EXTRACTION
|
|
40
|
+
Composants extraits de MostaGare (`src/components/license/*`, `lib/license-service`, `lib/licenseClient`)
|
|
41
|
+
PUIS GÉNÉRALISÉS : URLs serveur & projet codés en dur remplacés par la config .env — les composants
|
|
42
|
+
React lisent serverUrl/project via useLicenseUi() (<LicenseProvider> ← createLicenseUiFromEnv) ; les
|
|
43
|
+
libs lisent process.env.LICENSE_SERVER_URL / LICENSE_PROJECT (avec repli). Plus aucun 'MostaGare'/'gestigare'.
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/licensing-ui-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "UI de licence (gate d'accès, activation, configuration) — extraite de MostaGare, compose @mostajs/licensing (vérif RSA + client serveur de licences).",
|
|
5
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
|
+
"license": "AGPL-3.0-or-later",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.ts",
|
|
11
|
+
"./react/*": "./src/react/*"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"README.md",
|
|
16
|
+
"llms.txt"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"license",
|
|
20
|
+
"licence",
|
|
21
|
+
"activation",
|
|
22
|
+
"ui",
|
|
23
|
+
"react",
|
|
24
|
+
"mostajs"
|
|
25
|
+
],
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@mostajs/licensing": ">=0.1.0",
|
|
28
|
+
"react": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"react": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"lucide-react": ">=0.300.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @mostajs/licensing-ui — couche de PRÉSENTATION des licences.
|
|
2
|
+
// Composants React EXTRAITS de SolutionCh/MostaGare (gate, activation, configuration) +
|
|
3
|
+
// contrôleur headless qui COMPOSE @mostajs/licensing (client serveur + vérif RSA). DEVRULES §10.
|
|
4
|
+
// @author Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
|
|
6
|
+
// Contexte de configuration (.env) — les composants le consomment via useLicenseUi().
|
|
7
|
+
export { LicenseProvider, useLicenseUi } from './react/LicenseProvider';
|
|
8
|
+
|
|
9
|
+
// Composants React (extraits de MostaGare) — réutilisables par les apps Next/React.
|
|
10
|
+
export { default as LicenseGate } from './react/LicenseGate';
|
|
11
|
+
export { default as EnhancedLicenseGate } from './react/EnhancedLicenseGate';
|
|
12
|
+
export { default as LicenseActivationForm } from './react/LicenseActivationForm';
|
|
13
|
+
export { default as SimpleLicenseActivation } from './react/SimpleLicenseActivation';
|
|
14
|
+
export { default as PendingLicenseActivation } from './react/PendingLicenseActivation';
|
|
15
|
+
export { default as LicenseConfigForm } from './react/LicenseConfigForm';
|
|
16
|
+
|
|
17
|
+
import { createLicenseClient, checkLicense } from '@mostajs/licensing';
|
|
18
|
+
|
|
19
|
+
export interface LicenseUiOptions {
|
|
20
|
+
/** URL du serveur de licences (ex. https://licences.amia.fr). */
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
/** Projet (ex. 'salsabil', 'MostaGare'). */
|
|
23
|
+
project?: string;
|
|
24
|
+
/** Clé publique PEM pour vérifier les signatures. */
|
|
25
|
+
publicKeyPem?: string;
|
|
26
|
+
/** fetch injectable (défaut: global). */
|
|
27
|
+
fetch?: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Contrôleur headless — COMPOSE @mostajs/licensing (pas de logique réécrite) :
|
|
32
|
+
* client du serveur de licences + vérification de signature/expiration. Les composants
|
|
33
|
+
* React s'y branchent (un seul point d'intégration, testable sans navigateur).
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Construit les options depuis l'environnement (.env) — CONFIG PILOTÉE PAR .env :
|
|
37
|
+
* LICENSE_SERVER_URL (défaut https://licences.amia.fr)
|
|
38
|
+
* LICENSE_PROJECT (ex. salsabil)
|
|
39
|
+
* LICENSE_PUBLIC_KEY (clé publique PEM en base64)
|
|
40
|
+
*/
|
|
41
|
+
export function licenseUiConfigFromEnv(env: Record<string, string | undefined> = {}): LicenseUiOptions {
|
|
42
|
+
return {
|
|
43
|
+
baseUrl: env.LICENSE_SERVER_URL || 'https://licences.amia.fr',
|
|
44
|
+
project: env.LICENSE_PROJECT || undefined,
|
|
45
|
+
publicKeyPem: env.LICENSE_PUBLIC_KEY ? Buffer.from(env.LICENSE_PUBLIC_KEY, 'base64').toString('utf8') : undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Raccourci : contrôleur configuré entièrement depuis .env. */
|
|
50
|
+
export function createLicenseUiFromEnv(env: Record<string, string | undefined> = {}) {
|
|
51
|
+
return createLicenseUi(licenseUiConfigFromEnv(env));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createLicenseUi(opts: LicenseUiOptions) {
|
|
55
|
+
const client = createLicenseClient({ baseUrl: opts.baseUrl, publicKeyPem: opts.publicKeyPem, fetch: opts.fetch });
|
|
56
|
+
return {
|
|
57
|
+
client,
|
|
58
|
+
/** Vérifie une licence signée (signature RSA + projet + expiration). */
|
|
59
|
+
check: (signed: any) => checkLicense(signed, { project: opts.project, publicKeyPem: opts.publicKeyPem ?? '' }),
|
|
60
|
+
/** Demande un code d'activation par email. */
|
|
61
|
+
requestCode: (p: { email: string; company?: string; message?: string }) =>
|
|
62
|
+
client.requestActivationCode({ project: opts.project ?? '', ...p }),
|
|
63
|
+
/** Active une machine : télécharge la licence du code, la vérifie, notifie. */
|
|
64
|
+
activate: (p: { activationCode: string; machineId: string; machineName?: string }) =>
|
|
65
|
+
client.activate({ project: opts.project, ...p }),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
export interface LicenseInfo {
|
|
7
|
+
key: string;
|
|
8
|
+
email: string;
|
|
9
|
+
project: string;
|
|
10
|
+
expires_at: string;
|
|
11
|
+
activations: Activation[];
|
|
12
|
+
max_activations: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Activation {
|
|
16
|
+
machine_id: string;
|
|
17
|
+
machine_name: string;
|
|
18
|
+
activated_at: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LicenseStatus {
|
|
22
|
+
valid: boolean;
|
|
23
|
+
status: 'valid' | 'invalid' | 'expired' | 'error';
|
|
24
|
+
reason?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
license?: LicenseInfo;
|
|
27
|
+
remaining_days?: number;
|
|
28
|
+
remaining_activations?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class LicenseService {
|
|
32
|
+
private licenseDir: string;
|
|
33
|
+
private publicKeyPath: string;
|
|
34
|
+
private licensePath: string | null = null;
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
this.licenseDir = path.join(process.cwd(), "licences");
|
|
38
|
+
this.publicKeyPath = path.join(this.licenseDir, "public.pem");
|
|
39
|
+
this.findLicenseFile();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Trouve le fichier de licence signé
|
|
44
|
+
*/
|
|
45
|
+
private findLicenseFile(): void {
|
|
46
|
+
if (fs.existsSync(this.licenseDir)) {
|
|
47
|
+
const files = fs.readdirSync(this.licenseDir);
|
|
48
|
+
const licenseFile = files.find(file => file.endsWith('_licence.signed.json'));
|
|
49
|
+
|
|
50
|
+
if (licenseFile) {
|
|
51
|
+
this.licensePath = path.join(this.licenseDir, licenseFile);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Obtient l'ID unique de la machine
|
|
58
|
+
*/
|
|
59
|
+
public getMachineId(): string {
|
|
60
|
+
const networkInterfaces = os.networkInterfaces();
|
|
61
|
+
const macAddresses: string[] = [];
|
|
62
|
+
|
|
63
|
+
for (const interfaceName in networkInterfaces) {
|
|
64
|
+
const interfaces = networkInterfaces[interfaceName];
|
|
65
|
+
if (interfaces) {
|
|
66
|
+
for (const iface of interfaces) {
|
|
67
|
+
if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
|
68
|
+
macAddresses.push(iface.mac);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Utiliser la première adresse MAC ou un ID basé sur le hostname
|
|
75
|
+
const machineId = macAddresses.length > 0
|
|
76
|
+
? macAddresses[0].replace(/:/g, '').toUpperCase()
|
|
77
|
+
: crypto.createHash('sha256').update(os.hostname()).digest('hex').substring(0, 12).toUpperCase();
|
|
78
|
+
|
|
79
|
+
return machineId;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Obtient le nom de la machine
|
|
84
|
+
*/
|
|
85
|
+
public getMachineName(): string {
|
|
86
|
+
return os.hostname();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Vérifie la signature de la licence
|
|
91
|
+
*/
|
|
92
|
+
private verifySignature(signedData: any, publicKey: string): boolean {
|
|
93
|
+
try {
|
|
94
|
+
// Vérification temporaire pour les signatures du serveur web
|
|
95
|
+
// Détection générique basée sur le format du code d'activation
|
|
96
|
+
const isWebServerLicense = signedData.license.key &&
|
|
97
|
+
/^[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$/.test(signedData.license.key);
|
|
98
|
+
|
|
99
|
+
if (isWebServerLicense) {
|
|
100
|
+
console.log("Code généré par serveur web détecté (format XXXX-XXXX-XXXX-XXXX), validation RSA appliquée");
|
|
101
|
+
// Continuer avec la vérification RSA normale pour les codes du serveur web
|
|
102
|
+
} else if (signedData.signature && signedData.signature.includes(signedData.license.project)) {
|
|
103
|
+
console.log("Signature temporaire détectée, validation bypassed pour développement");
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Si la licence a des activations, vérifier sans les activations (signature originale)
|
|
108
|
+
const licenseForVerification = { ...signedData.license };
|
|
109
|
+
if (licenseForVerification.activations && licenseForVerification.activations.length > 0) {
|
|
110
|
+
// Créer une copie sans les activations pour vérifier la signature originale
|
|
111
|
+
delete licenseForVerification.activations;
|
|
112
|
+
console.log("Licence avec activations détectée, vérification sans les activations");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Reconstruire le JSON canonique (comme côté Python)
|
|
116
|
+
const sortedKeys = Object.keys(licenseForVerification).sort();
|
|
117
|
+
const sortedLicense: any = {};
|
|
118
|
+
for (const key of sortedKeys) {
|
|
119
|
+
sortedLicense[key] = licenseForVerification[key];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const canonicalPayload = JSON.stringify(sortedLicense);
|
|
123
|
+
|
|
124
|
+
// Vérifier la signature
|
|
125
|
+
const verify = crypto.createVerify("RSA-SHA256");
|
|
126
|
+
verify.update(canonicalPayload, "utf8");
|
|
127
|
+
verify.end();
|
|
128
|
+
|
|
129
|
+
const signatureBuffer = Buffer.from(signedData.signature, "hex");
|
|
130
|
+
return verify.verify(publicKey, signatureBuffer);
|
|
131
|
+
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error("Erreur vérification signature:", error);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Vérifie la licence complète
|
|
140
|
+
*/
|
|
141
|
+
public async verifyLicense(): Promise<LicenseStatus> {
|
|
142
|
+
try {
|
|
143
|
+
// Vérifier l'existence des fichiers
|
|
144
|
+
if (!this.licensePath || !fs.existsSync(this.licensePath)) {
|
|
145
|
+
return {
|
|
146
|
+
valid: false,
|
|
147
|
+
status: 'invalid',
|
|
148
|
+
reason: 'license_not_found',
|
|
149
|
+
message: 'Aucun fichier de licence trouvé'
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!fs.existsSync(this.publicKeyPath)) {
|
|
154
|
+
return {
|
|
155
|
+
valid: false,
|
|
156
|
+
status: 'invalid',
|
|
157
|
+
reason: 'public_key_not_found',
|
|
158
|
+
message: 'Clé publique non trouvée'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Lire les fichiers
|
|
163
|
+
const signedData = JSON.parse(fs.readFileSync(this.licensePath, "utf8"));
|
|
164
|
+
const publicKey = fs.readFileSync(this.publicKeyPath, "utf8");
|
|
165
|
+
|
|
166
|
+
// Vérifier la structure
|
|
167
|
+
if (!signedData.license || !signedData.signature) {
|
|
168
|
+
return {
|
|
169
|
+
valid: false,
|
|
170
|
+
status: 'invalid',
|
|
171
|
+
reason: 'invalid_format',
|
|
172
|
+
message: 'Format de licence invalide'
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Vérifier la signature
|
|
177
|
+
if (!this.verifySignature(signedData, publicKey)) {
|
|
178
|
+
return {
|
|
179
|
+
valid: false,
|
|
180
|
+
status: 'invalid',
|
|
181
|
+
reason: 'invalid_signature',
|
|
182
|
+
message: 'Signature invalide'
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Vérifier la date d'expiration
|
|
187
|
+
const now = new Date();
|
|
188
|
+
const expiryDate = new Date(signedData.license.expires_at);
|
|
189
|
+
const timeDiff = expiryDate.getTime() - now.getTime();
|
|
190
|
+
const remainingDays = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
|
191
|
+
|
|
192
|
+
if (now > expiryDate) {
|
|
193
|
+
return {
|
|
194
|
+
valid: false,
|
|
195
|
+
status: 'expired',
|
|
196
|
+
reason: 'license_expired',
|
|
197
|
+
message: `Licence expirée depuis ${-remainingDays} jours`,
|
|
198
|
+
license: signedData.license,
|
|
199
|
+
remaining_days: remainingDays
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Vérifier le nombre d'activations
|
|
204
|
+
const activations = signedData.license.activations || [];
|
|
205
|
+
const maxActivations = signedData.license.max_activations || 3;
|
|
206
|
+
|
|
207
|
+
// Vérifier si cette machine est activée
|
|
208
|
+
const machineId = this.getMachineId();
|
|
209
|
+
const isThisMachineActivated = activations.some((a: Activation) => a.machine_id === machineId);
|
|
210
|
+
|
|
211
|
+
if (!isThisMachineActivated && activations.length >= maxActivations) {
|
|
212
|
+
return {
|
|
213
|
+
valid: false,
|
|
214
|
+
status: 'invalid',
|
|
215
|
+
reason: 'max_activations_reached',
|
|
216
|
+
message: `Nombre maximum d'activations atteint (${maxActivations})`,
|
|
217
|
+
license: signedData.license
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Licence valide !
|
|
222
|
+
return {
|
|
223
|
+
valid: true,
|
|
224
|
+
status: 'valid',
|
|
225
|
+
message: 'Licence valide et active',
|
|
226
|
+
license: signedData.license,
|
|
227
|
+
remaining_days: remainingDays,
|
|
228
|
+
remaining_activations: maxActivations - activations.length
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
} catch (error: any) {
|
|
232
|
+
return {
|
|
233
|
+
valid: false,
|
|
234
|
+
status: 'error',
|
|
235
|
+
reason: 'verification_error',
|
|
236
|
+
message: error.message || 'Erreur lors de la vérification'
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Notifie le serveur qu'une activation a eu lieu
|
|
243
|
+
*/
|
|
244
|
+
private async notifyServerActivation(activationCode: string, machineId: string, machineName: string): Promise<void> {
|
|
245
|
+
try {
|
|
246
|
+
const { exec } = require('child_process');
|
|
247
|
+
const util = require('util');
|
|
248
|
+
const execPromise = util.promisify(exec);
|
|
249
|
+
|
|
250
|
+
const curlCommand = `curl -k -X POST ${process.env.LICENSE_SERVER_URL || "https://licences.amia.fr"}/api/notify-activation \
|
|
251
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
252
|
+
-d "activationCode=${activationCode}&machineId=${machineId}&machineName=${machineName}"`;
|
|
253
|
+
|
|
254
|
+
await execPromise(curlCommand);
|
|
255
|
+
console.log("Activation notifiée au serveur avec succès");
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.warn("Erreur lors de la notification au serveur:", error);
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Active la licence pour cette machine
|
|
264
|
+
*/
|
|
265
|
+
public async activateMachine(): Promise<{ success: boolean; message: string; activation?: Activation }> {
|
|
266
|
+
try {
|
|
267
|
+
if (!this.licensePath || !fs.existsSync(this.licensePath)) {
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
message: 'Licence non trouvée'
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const signedData = JSON.parse(fs.readFileSync(this.licensePath, "utf8"));
|
|
275
|
+
const activations = signedData.license.activations || [];
|
|
276
|
+
const maxActivations = signedData.license.max_activations || 3;
|
|
277
|
+
|
|
278
|
+
const machineId = this.getMachineId();
|
|
279
|
+
const machineName = this.getMachineName();
|
|
280
|
+
|
|
281
|
+
// Vérifier si déjà activée
|
|
282
|
+
const existingActivation = activations.find((a: Activation) => a.machine_id === machineId);
|
|
283
|
+
if (existingActivation) {
|
|
284
|
+
return {
|
|
285
|
+
success: true,
|
|
286
|
+
message: 'Machine déjà activée',
|
|
287
|
+
activation: existingActivation
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Vérifier la limite
|
|
292
|
+
if (activations.length >= maxActivations) {
|
|
293
|
+
return {
|
|
294
|
+
success: false,
|
|
295
|
+
message: `Limite d'activations atteinte (${maxActivations})`
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Ajouter l'activation
|
|
300
|
+
const newActivation: Activation = {
|
|
301
|
+
machine_id: machineId,
|
|
302
|
+
machine_name: machineName,
|
|
303
|
+
activated_at: new Date().toISOString()
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
activations.push(newActivation);
|
|
307
|
+
signedData.license.activations = activations;
|
|
308
|
+
|
|
309
|
+
// Sauvegarder (Note: invalide la signature)
|
|
310
|
+
fs.writeFileSync(this.licensePath, JSON.stringify(signedData, null, 2));
|
|
311
|
+
|
|
312
|
+
// Notifier le serveur de l'activation
|
|
313
|
+
try {
|
|
314
|
+
const activationCode = signedData.license.key;
|
|
315
|
+
if (activationCode) {
|
|
316
|
+
await this.notifyServerActivation(activationCode, machineId, machineName);
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.warn("Impossible de notifier le serveur de l'activation:", error);
|
|
320
|
+
// Ne pas faire échouer l'activation pour autant
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
success: true,
|
|
325
|
+
message: 'Machine activée avec succès',
|
|
326
|
+
activation: newActivation
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
} catch (error: any) {
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
message: error.message || 'Erreur lors de l\'activation'
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Obtient les informations de licence de manière sécurisée
|
|
339
|
+
*/
|
|
340
|
+
public async getLicenseInfo(): Promise<LicenseInfo | null> {
|
|
341
|
+
const status = await this.verifyLicense();
|
|
342
|
+
return status.license || null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Vérifie si la licence est valide et cette machine est activée
|
|
347
|
+
*/
|
|
348
|
+
public async isLicenseValidAndActivated(): Promise<boolean> {
|
|
349
|
+
const status = await this.verifyLicense();
|
|
350
|
+
if (!status.valid) return false;
|
|
351
|
+
|
|
352
|
+
const machineId = this.getMachineId();
|
|
353
|
+
const activations = status.license?.activations || [];
|
|
354
|
+
|
|
355
|
+
return activations.some(a => a.machine_id === machineId);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Export une instance singleton
|
|
360
|
+
export const licenseService = new LicenseService();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import { hostname } from "os";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
|
|
8
|
+
// Config pilotée par .env : LICENSE_SERVER_URL, LICENSE_PROJECT (avec repli paramètre).
|
|
9
|
+
const SERVER_URL = (process.env.LICENSE_SERVER_URL || "https://licences.amia.fr").replace(/\/+$/, "");
|
|
10
|
+
const DEFAULT_PROJECT = process.env.LICENSE_PROJECT || "app";
|
|
11
|
+
|
|
12
|
+
export async function requestLicenseFromClient(email: string, project = DEFAULT_PROJECT, version = "1.0.0") {
|
|
13
|
+
const device_id = crypto.createHash("sha256")
|
|
14
|
+
.update(os.hostname() + os.platform())
|
|
15
|
+
.digest("hex");
|
|
16
|
+
|
|
17
|
+
const data = {
|
|
18
|
+
email,
|
|
19
|
+
project,
|
|
20
|
+
version,
|
|
21
|
+
device_id,
|
|
22
|
+
os_info: `${os.platform()} ${os.release()}`,
|
|
23
|
+
hostname: hostname()
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const res = await axios.post(`${SERVER_URL}/api/licenses/generate-from-client`, data, {
|
|
28
|
+
responseType: "blob"
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const dir = path.join(process.cwd(), "licences");
|
|
32
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
|
33
|
+
const filePath = path.join(dir, `${project}_licence.signed.json`);
|
|
34
|
+
fs.writeFileSync(filePath, res.data);
|
|
35
|
+
return { success: true, filePath };
|
|
36
|
+
} catch (error: any) {
|
|
37
|
+
console.error("❌ Erreur :", error.response?.data || error.message);
|
|
38
|
+
return { success: false, error };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function validateLicense(licensePath: string, pubkeyPath: string) {
|
|
42
|
+
try {
|
|
43
|
+
const license = JSON.parse(fs.readFileSync(licensePath, "utf8"));
|
|
44
|
+
const pubKey = fs.readFileSync(pubkeyPath, "utf8");
|
|
45
|
+
|
|
46
|
+
const verify = crypto.createVerify("SHA256");
|
|
47
|
+
verify.update(JSON.stringify(license.license));
|
|
48
|
+
verify.end();
|
|
49
|
+
|
|
50
|
+
const isValid = verify.verify(pubKey, Buffer.from(license.signature, "hex"));
|
|
51
|
+
|
|
52
|
+
if (isValid) {
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
if (now < license.license.expires_at) {
|
|
55
|
+
return { valid: true };
|
|
56
|
+
} else {
|
|
57
|
+
return { valid: false, reason: "expired" };
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
return { valid: false, reason: "invalid_signature" };
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error("Erreur licence :", e);
|
|
64
|
+
return { valid: false, reason: "error", error: e };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function checkLicenseValidity() {
|
|
68
|
+
const licensePath = path.join(process.cwd(), "licences", `${DEFAULT_PROJECT}_licence.signed.json`);
|
|
69
|
+
const pubkeyPath = path.join(process.cwd(), "licences", "public.pem");
|
|
70
|
+
|
|
71
|
+
if (!fs.existsSync(licensePath) || !fs.existsSync(pubkeyPath)) {
|
|
72
|
+
return { valid: false, reason: "missing_files" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return await validateLicense(licensePath, pubkeyPath);
|
|
76
|
+
}
|