@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.
@@ -0,0 +1,365 @@
1
+ "use client";
2
+ import { useState, useRef } from 'react';
3
+ import { Upload, Shield, CheckCircle, XCircle, AlertTriangle, Loader2, Eye, EyeOff } from 'lucide-react';
4
+ import { useLicenseUi } from './LicenseProvider';
5
+
6
+ interface LicenseActivationFormProps {
7
+ onActivationSuccess?: (licenseInfo: any) => void;
8
+ onBack?: () => void;
9
+ }
10
+
11
+ interface ActivationResponse {
12
+ success: boolean;
13
+ message?: string;
14
+ error?: string;
15
+ licenseInfo?: {
16
+ email: string;
17
+ project: string;
18
+ expires_at: string;
19
+ activations: number;
20
+ max_activations: number;
21
+ key: string;
22
+ };
23
+ remaining_days?: number;
24
+ }
25
+
26
+ export default function LicenseActivationForm({
27
+ onActivationSuccess,
28
+ onBack
29
+ }: LicenseActivationFormProps) {
30
+ const { serverUrl } = useLicenseUi(); // config .env (createLicenseUiFromEnv)
31
+ const [activationCode, setActivationCode] = useState('');
32
+ const [licenseFile, setLicenseFile] = useState<File | null>(null);
33
+ const [loading, setLoading] = useState(false);
34
+ const [error, setError] = useState<string | null>(null);
35
+ const [success, setSuccess] = useState<string | null>(null);
36
+ const [showCode, setShowCode] = useState(false);
37
+ const fileInputRef = useRef<HTMLInputElement>(null);
38
+
39
+ const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
40
+ const file = event.target.files?.[0];
41
+ if (file) {
42
+ // Vérifier l'extension
43
+ if (!file.name.endsWith('.json')) {
44
+ setError('Veuillez sélectionner un fichier de licence valide (.json)');
45
+ return;
46
+ }
47
+
48
+ // Vérifier la taille (max 1MB)
49
+ if (file.size > 1024 * 1024) {
50
+ setError('Le fichier de licence est trop volumineux (max 1MB)');
51
+ return;
52
+ }
53
+
54
+ setLicenseFile(file);
55
+ setError(null);
56
+ }
57
+ };
58
+
59
+ const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
60
+ event.preventDefault();
61
+ const file = event.dataTransfer.files[0];
62
+ if (file) {
63
+ if (!file.name.endsWith('.json')) {
64
+ setError('Veuillez déposer un fichier de licence valide (.json)');
65
+ return;
66
+ }
67
+ setLicenseFile(file);
68
+ setError(null);
69
+ }
70
+ };
71
+
72
+ const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
73
+ event.preventDefault();
74
+ };
75
+
76
+ const validateActivationCode = (code: string): boolean => {
77
+ // Format attendu: XXXX-XXXX-XXXX-XXXX (16 caractères + 3 tirets)
78
+ const codeRegex = /^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/;
79
+ return codeRegex.test(code.toUpperCase());
80
+ };
81
+
82
+ const handleActivation = async () => {
83
+ if (!licenseFile) {
84
+ setError('Veuillez sélectionner un fichier de licence');
85
+ return;
86
+ }
87
+
88
+ if (!activationCode.trim()) {
89
+ setError('Veuillez entrer votre code d\'activation');
90
+ return;
91
+ }
92
+
93
+ if (!validateActivationCode(activationCode)) {
94
+ setError('Format de code d\'activation invalide (attendu: XXXX-XXXX-XXXX-XXXX)');
95
+ return;
96
+ }
97
+
98
+ setLoading(true);
99
+ setError(null);
100
+ setSuccess(null);
101
+
102
+ try {
103
+ // Lire le fichier de licence
104
+ const licenseContent = await licenseFile.text();
105
+ let licenseData;
106
+
107
+ try {
108
+ licenseData = JSON.parse(licenseContent);
109
+ } catch {
110
+ setError('Le fichier de licence n\'est pas au format JSON valide');
111
+ setLoading(false);
112
+ return;
113
+ }
114
+
115
+ // Préparer les données d'activation
116
+ const formData = new FormData();
117
+ formData.append('activationCode', activationCode.toUpperCase());
118
+ formData.append('licenseData', JSON.stringify(licenseData));
119
+
120
+ // Envoyer la demande d'activation
121
+ const response = await fetch(`${serverUrl}/api/activate`, {
122
+ method: 'POST',
123
+ body: formData,
124
+ });
125
+
126
+ const result: ActivationResponse = await response.json();
127
+
128
+ if (!response.ok) {
129
+ throw new Error(result.error || 'Erreur lors de l\'activation');
130
+ }
131
+
132
+ if (result.success) {
133
+ setSuccess('Licence activée avec succès !');
134
+
135
+ // Sauvegarder localement les informations de licence
136
+ if (result.licenseInfo) {
137
+ localStorage.setItem('license_cache', JSON.stringify({
138
+ licenseInfo: result,
139
+ timestamp: new Date().toISOString(),
140
+ valid: true
141
+ }));
142
+
143
+ // Sauvegarder aussi le fichier de licence localement pour référence future
144
+ await fetch('/api/license/save-local', {
145
+ method: 'POST',
146
+ headers: {
147
+ 'Content-Type': 'application/json',
148
+ },
149
+ body: JSON.stringify({
150
+ licenseData,
151
+ activationCode: activationCode.toUpperCase()
152
+ }),
153
+ });
154
+ }
155
+
156
+ // Callback de succès après un court délai
157
+ setTimeout(() => {
158
+ onActivationSuccess?.(result.licenseInfo);
159
+ }, 1500);
160
+ } else {
161
+ setError(result.message || result.error || 'Échec de l\'activation');
162
+ }
163
+ } catch (err: any) {
164
+ console.error('Erreur d\'activation:', err);
165
+ if (err.name === 'TypeError' && err.message.includes('fetch')) {
166
+ setError('Impossible de se connecter au serveur de licences. Vérifiez votre connexion internet.');
167
+ } else {
168
+ setError(err.message || 'Erreur lors de l\'activation de la licence');
169
+ }
170
+ } finally {
171
+ setLoading(false);
172
+ }
173
+ };
174
+
175
+ const clearForm = () => {
176
+ setActivationCode('');
177
+ setLicenseFile(null);
178
+ setError(null);
179
+ setSuccess(null);
180
+ if (fileInputRef.current) {
181
+ fileInputRef.current.value = '';
182
+ }
183
+ };
184
+
185
+ return (
186
+ <div className="max-w-lg mx-auto p-8 bg-white rounded-xl shadow-lg">
187
+ <div className="text-center mb-8">
188
+ <Shield className="w-16 h-16 text-blue-500 mx-auto mb-4" />
189
+ <h2 className="text-2xl font-bold text-gray-800 mb-2">Activer votre licence</h2>
190
+ <p className="text-gray-600">
191
+ Uploadez votre fichier de licence et entrez votre code d'activation
192
+ </p>
193
+ </div>
194
+
195
+ {/* Messages d'état */}
196
+ {error && (
197
+ <div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
198
+ <div className="flex items-center">
199
+ <XCircle className="w-5 h-5 text-red-500 mr-2 flex-shrink-0" />
200
+ <p className="text-red-700 text-sm">{error}</p>
201
+ </div>
202
+ </div>
203
+ )}
204
+
205
+ {success && (
206
+ <div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
207
+ <div className="flex items-center">
208
+ <CheckCircle className="w-5 h-5 text-green-500 mr-2 flex-shrink-0" />
209
+ <p className="text-green-700 text-sm">{success}</p>
210
+ </div>
211
+ </div>
212
+ )}
213
+
214
+ {/* Zone d'upload de fichier */}
215
+ <div className="mb-6">
216
+ <label className="block text-sm font-medium text-gray-700 mb-3">
217
+ Fichier de licence *
218
+ </label>
219
+ <div
220
+ onDrop={handleDrop}
221
+ onDragOver={handleDragOver}
222
+ className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
223
+ licenseFile
224
+ ? 'border-green-300 bg-green-50'
225
+ : 'border-gray-300 hover:border-blue-400 hover:bg-blue-50'
226
+ }`}
227
+ onClick={() => fileInputRef.current?.click()}
228
+ >
229
+ <input
230
+ ref={fileInputRef}
231
+ type="file"
232
+ accept=".json"
233
+ onChange={handleFileSelect}
234
+ className="hidden"
235
+ />
236
+
237
+ {licenseFile ? (
238
+ <div className="flex items-center justify-center">
239
+ <CheckCircle className="w-8 h-8 text-green-500 mr-3" />
240
+ <div className="text-left">
241
+ <p className="text-sm font-medium text-green-700">{licenseFile.name}</p>
242
+ <p className="text-xs text-green-600">
243
+ {(licenseFile.size / 1024).toFixed(1)} KB
244
+ </p>
245
+ </div>
246
+ </div>
247
+ ) : (
248
+ <div>
249
+ <Upload className="w-12 h-12 text-gray-400 mx-auto mb-3" />
250
+ <p className="text-sm text-gray-600 mb-2">
251
+ <span className="font-medium">Cliquez pour sélectionner</span> ou glissez votre fichier de licence
252
+ </p>
253
+ <p className="text-xs text-gray-400">
254
+ Formats supportés: .json (max 1MB)
255
+ </p>
256
+ </div>
257
+ )}
258
+ </div>
259
+ </div>
260
+
261
+ {/* Code d'activation */}
262
+ <div className="mb-6">
263
+ <label className="block text-sm font-medium text-gray-700 mb-3">
264
+ Code d'activation *
265
+ </label>
266
+ <div className="relative">
267
+ <input
268
+ type={showCode ? 'text' : 'password'}
269
+ value={activationCode}
270
+ onChange={(e) => {
271
+ // Auto-format with dashes
272
+ let value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
273
+ if (value.length > 4) value = value.slice(0, 4) + '-' + value.slice(4);
274
+ if (value.length > 9) value = value.slice(0, 9) + '-' + value.slice(9);
275
+ if (value.length > 14) value = value.slice(0, 14) + '-' + value.slice(14);
276
+ if (value.length > 19) value = value.slice(0, 19);
277
+ setActivationCode(value);
278
+ }}
279
+ placeholder="XXXX-XXXX-XXXX-XXXX"
280
+ maxLength={19}
281
+ className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono text-center tracking-widest"
282
+ />
283
+ <button
284
+ type="button"
285
+ onClick={() => setShowCode(!showCode)}
286
+ className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
287
+ >
288
+ {showCode ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
289
+ </button>
290
+ </div>
291
+ <p className="text-xs text-gray-500 mt-2">
292
+ Entrez le code fourni avec votre licence (format: XXXX-XXXX-XXXX-XXXX)
293
+ </p>
294
+ </div>
295
+
296
+ {/* Informations importantes */}
297
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
298
+ <div className="flex items-start">
299
+ <AlertTriangle className="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
300
+ <div>
301
+ <h4 className="text-sm font-medium text-blue-800 mb-2">Où trouver vos éléments de licence ?</h4>
302
+ <div className="text-xs text-blue-700 space-y-1">
303
+ <p>• <strong>Fichier de licence :</strong> Téléchargé depuis votre espace client sur licences.amia.fr</p>
304
+ <p>• <strong>Code d'activation :</strong> Fourni par email lors de l'achat de votre licence</p>
305
+ <p>• <strong>Connexion requise :</strong> Internet nécessaire pour l'activation initiale</p>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </div>
310
+
311
+ {/* Boutons d'action */}
312
+ <div className="space-y-3">
313
+ <button
314
+ onClick={handleActivation}
315
+ disabled={!licenseFile || !activationCode.trim() || loading}
316
+ className="w-full px-4 py-3 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center"
317
+ >
318
+ {loading ? (
319
+ <>
320
+ <Loader2 className="w-5 h-5 animate-spin mr-2" />
321
+ Activation en cours...
322
+ </>
323
+ ) : (
324
+ 'Activer la licence'
325
+ )}
326
+ </button>
327
+
328
+ <div className="flex space-x-3">
329
+ <button
330
+ onClick={clearForm}
331
+ disabled={loading}
332
+ className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 transition-colors"
333
+ >
334
+ Effacer
335
+ </button>
336
+
337
+ {onBack && (
338
+ <button
339
+ onClick={onBack}
340
+ disabled={loading}
341
+ className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 transition-colors"
342
+ >
343
+ Retour
344
+ </button>
345
+ )}
346
+ </div>
347
+ </div>
348
+
349
+ {/* Lien vers le site de licences */}
350
+ <div className="mt-6 pt-6 border-t border-gray-200 text-center">
351
+ <p className="text-xs text-gray-500 mb-2">
352
+ Vous n'avez pas encore de licence ?
353
+ </p>
354
+ <a
355
+ href={serverUrl}
356
+ target="_blank"
357
+ rel="noopener noreferrer"
358
+ className="text-sm text-blue-500 hover:text-blue-700 font-medium"
359
+ >
360
+ Visiter licences.amia.fr →
361
+ </a>
362
+ </div>
363
+ </div>
364
+ );
365
+ }
@@ -0,0 +1,243 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Save, RefreshCw, AlertCircle, CheckCircle, Users } from 'lucide-react';
5
+
6
+ interface LicenseInfo {
7
+ valid: boolean;
8
+ license?: {
9
+ key: string;
10
+ email: string;
11
+ project: string;
12
+ expires_at: string;
13
+ activations: any[];
14
+ max_activations: number;
15
+ };
16
+ remaining_days?: number;
17
+ }
18
+
19
+ export default function LicenseConfigForm() {
20
+ const [licenseInfo, setLicenseInfo] = useState<LicenseInfo | null>(null);
21
+ const [maxActivations, setMaxActivations] = useState<number>(4);
22
+ const [loading, setLoading] = useState(true);
23
+ const [saving, setSaving] = useState(false);
24
+ const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
25
+
26
+ // Charger les informations de licence actuelles
27
+ const fetchLicenseInfo = async () => {
28
+ try {
29
+ setLoading(true);
30
+ const response = await fetch('/api/license/verify');
31
+ const data = await response.json();
32
+ setLicenseInfo(data);
33
+
34
+ if (data.license) {
35
+ setMaxActivations(data.license.max_activations || 4);
36
+ }
37
+ } catch (error) {
38
+ console.error('Erreur lors du chargement de la licence:', error);
39
+ setMessage({ type: 'error', text: 'Erreur lors du chargement des informations de licence' });
40
+ } finally {
41
+ setLoading(false);
42
+ }
43
+ };
44
+
45
+ useEffect(() => {
46
+ fetchLicenseInfo();
47
+ }, []);
48
+
49
+ const handleSave = async () => {
50
+ setSaving(true);
51
+ setMessage(null);
52
+
53
+ try {
54
+ const response = await fetch('/api/admin/license/update-max-activations', {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ },
59
+ body: JSON.stringify({
60
+ max_activations: maxActivations
61
+ })
62
+ });
63
+
64
+ const result = await response.json();
65
+
66
+ if (result.success) {
67
+ setMessage({ type: 'success', text: 'Configuration mise à jour avec succès' });
68
+ await fetchLicenseInfo(); // Recharger les informations
69
+ } else {
70
+ setMessage({ type: 'error', text: result.error || 'Erreur lors de la mise à jour' });
71
+ }
72
+ } catch (error) {
73
+ console.error('Erreur lors de la sauvegarde:', error);
74
+ setMessage({ type: 'error', text: 'Erreur réseau lors de la sauvegarde' });
75
+ } finally {
76
+ setSaving(false);
77
+ }
78
+ };
79
+
80
+ if (loading) {
81
+ return (
82
+ <div className="flex justify-center items-center py-12">
83
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
84
+ </div>
85
+ );
86
+ }
87
+
88
+ return (
89
+ <div className="space-y-6">
90
+ {/* Message de statut */}
91
+ {message && (
92
+ <div className={`p-4 rounded-lg border flex items-center gap-3 ${
93
+ message.type === 'success'
94
+ ? 'bg-green-50 border-green-200 text-green-800'
95
+ : 'bg-red-50 border-red-200 text-red-800'
96
+ }`}>
97
+ {message.type === 'success' ? (
98
+ <CheckCircle className="w-5 h-5" />
99
+ ) : (
100
+ <AlertCircle className="w-5 h-5" />
101
+ )}
102
+ <span>{message.text}</span>
103
+ </div>
104
+ )}
105
+
106
+ {/* Informations licence actuelle */}
107
+ {licenseInfo && licenseInfo.license && (
108
+ <div className="bg-gray-50 rounded-lg p-6">
109
+ <h3 className="font-semibold text-gray-900 mb-4">Informations Licence Actuelle</h3>
110
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
111
+ <div>
112
+ <span className="font-medium text-gray-700">Email:</span>
113
+ <span className="ml-2 text-gray-900">{licenseInfo.license.email}</span>
114
+ </div>
115
+ <div>
116
+ <span className="font-medium text-gray-700">Projet:</span>
117
+ <span className="ml-2 text-gray-900">{licenseInfo.license.project}</span>
118
+ </div>
119
+ <div>
120
+ <span className="font-medium text-gray-700">Clé:</span>
121
+ <span className="ml-2 text-gray-900 font-mono text-xs">{licenseInfo.license.key}</span>
122
+ </div>
123
+ <div>
124
+ <span className="font-medium text-gray-700">Expire le:</span>
125
+ <span className="ml-2 text-gray-900">
126
+ {new Date(licenseInfo.license.expires_at).toLocaleDateString()}
127
+ </span>
128
+ </div>
129
+ <div>
130
+ <span className="font-medium text-gray-700">Postes actifs:</span>
131
+ <span className="ml-2 text-gray-900">
132
+ {licenseInfo.license.activations.length} / {licenseInfo.license.max_activations}
133
+ </span>
134
+ </div>
135
+ <div>
136
+ <span className="font-medium text-gray-700">Jours restants:</span>
137
+ <span className={`ml-2 font-medium ${
138
+ (licenseInfo.remaining_days || 0) < 30 ? 'text-yellow-600' : 'text-green-600'
139
+ }`}>
140
+ {licenseInfo.remaining_days || 0} jours
141
+ </span>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ )}
146
+
147
+ {/* Configuration du nombre maximum de postes */}
148
+ <div className="space-y-4">
149
+ <div>
150
+ <label className="block text-sm font-medium text-gray-700 mb-2">
151
+ Nombre maximum de postes autorisés
152
+ </label>
153
+ <div className="flex items-center gap-4">
154
+ <div className="flex-1 max-w-xs">
155
+ <input
156
+ type="number"
157
+ min="1"
158
+ max="10"
159
+ value={maxActivations}
160
+ onChange={(e) => setMaxActivations(Math.max(1, Math.min(10, parseInt(e.target.value) || 1)))}
161
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
162
+ />
163
+ </div>
164
+ <div className="text-sm text-gray-500">
165
+ <Users className="w-4 h-4 inline mr-1" />
166
+ postes maximum
167
+ </div>
168
+ </div>
169
+ <p className="text-sm text-gray-600 mt-2">
170
+ Cette valeur détermine combien de postes clients peuvent être activés simultanément avec cette licence.
171
+ Recommandé: 4 postes pour un usage normal.
172
+ </p>
173
+ </div>
174
+
175
+ {/* Indicateur d'utilisation */}
176
+ {licenseInfo && licenseInfo.license && (
177
+ <div className="bg-blue-50 rounded-lg p-4">
178
+ <h4 className="font-medium text-blue-900 mb-2">Utilisation actuelle</h4>
179
+ <div className="flex items-center gap-4">
180
+ <div className="flex-1">
181
+ <div className="w-full bg-blue-200 rounded-full h-2">
182
+ <div
183
+ className="bg-blue-600 h-2 rounded-full transition-all duration-300"
184
+ style={{
185
+ width: `${Math.min(100, (licenseInfo.license.activations.length / maxActivations) * 100)}%`
186
+ }}
187
+ ></div>
188
+ </div>
189
+ </div>
190
+ <div className="text-sm text-blue-700 font-medium">
191
+ {licenseInfo.license.activations.length} / {maxActivations}
192
+ </div>
193
+ </div>
194
+ <p className="text-sm text-blue-600 mt-2">
195
+ {maxActivations - licenseInfo.license.activations.length} postes disponibles
196
+ </p>
197
+ </div>
198
+ )}
199
+
200
+ {/* Actions */}
201
+ <div className="flex gap-4 pt-4">
202
+ <button
203
+ onClick={handleSave}
204
+ disabled={saving}
205
+ className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 flex items-center gap-2"
206
+ >
207
+ {saving ? (
208
+ <RefreshCw className="w-4 h-4 animate-spin" />
209
+ ) : (
210
+ <Save className="w-4 h-4" />
211
+ )}
212
+ {saving ? 'Sauvegarde...' : 'Sauvegarder'}
213
+ </button>
214
+
215
+ <button
216
+ onClick={fetchLicenseInfo}
217
+ disabled={loading}
218
+ className="px-6 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 flex items-center gap-2"
219
+ >
220
+ <RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
221
+ Actualiser
222
+ </button>
223
+ </div>
224
+
225
+ {/* Avertissement */}
226
+ <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
227
+ <div className="flex items-start gap-3">
228
+ <AlertCircle className="w-5 h-5 text-yellow-600 mt-0.5" />
229
+ <div className="text-sm text-yellow-800">
230
+ <p className="font-medium mb-1">Important:</p>
231
+ <ul className="space-y-1 list-disc list-inside">
232
+ <li>Cette modification affecte le nombre total de postes pouvant utiliser l'application</li>
233
+ <li>Les postes actuellement connectés ne seront pas déconnectés</li>
234
+ <li>La nouvelle limite s'appliquera aux nouvelles connexions</li>
235
+ <li>En cas de problème, contactez le support technique</li>
236
+ </ul>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ );
243
+ }