@mostajs/setup 1.0.0 → 1.1.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @mostajs/setup
2
2
 
3
- > Reusable setup wizard module — multi-dialect DB configuration, .env.local writer, seed runner.
3
+ > Reusable setup wizard module — multi-dialect DB configuration, .env.local writer, seed runner, module discovery.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@mostajs/setup.svg)](https://www.npmjs.com/package/@mostajs/setup)
6
6
  [![license](https://img.shields.io/npm/l/@mostajs/setup.svg)](LICENSE)
@@ -9,71 +9,986 @@ Part of the [@mosta suite](https://mostajs.dev).
9
9
 
10
10
  ---
11
11
 
12
- ## Installation
12
+ ## Table des matieres
13
+
14
+ 1. [Installation](#1-installation)
15
+ 2. [Architecture du package](#2-architecture-du-package)
16
+ 3. [Didacticiel : integrer @mostajs/setup dans une nouvelle app](#3-didacticiel--integrer-mostajssetup-dans-une-nouvelle-app)
17
+ - [Etape 1 — Installation des dependances](#etape-1--installation-des-dependances)
18
+ - [Etape 2 — Configurer le setup (lib/setup.ts)](#etape-2--configurer-le-setup-libsetupts)
19
+ - [Etape 3 — Creer les routes API](#etape-3--creer-les-routes-api)
20
+ - [Etape 4 — Creer la page Setup (frontend)](#etape-4--creer-la-page-setup-frontend)
21
+ - [Etape 5 — Middleware : rediriger vers /setup](#etape-5--middleware--rediriger-vers-setup)
22
+ - [Etape 6 — Tester le flux complet](#etape-6--tester-le-flux-complet)
23
+ 4. [API Reference](#4-api-reference)
24
+ 5. [Les 13 dialectes supportes](#5-les-13-dialectes-supportes)
25
+ 6. [Systeme de modules](#6-systeme-de-modules)
26
+ 7. [Exemples avances](#7-exemples-avances)
27
+ 8. [FAQ / Troubleshooting](#8-faq--troubleshooting)
28
+
29
+ ---
30
+
31
+ ## 1. Installation
13
32
 
14
33
  ```bash
15
34
  npm install @mostajs/setup @mostajs/orm
16
35
  ```
17
36
 
18
- ## Quick Start
37
+ `@mostajs/orm` est un peer dependency requis (il fournit les dialectes DB).
38
+
39
+ ---
40
+
41
+ ## 2. Architecture du package
42
+
43
+ ```
44
+ @mostajs/setup
45
+ ├── data/
46
+ │ ├── dialects.ts # Metadata des 13 SGBD (icone, port, driver)
47
+ │ └── module-definitions.ts # Liste statique des modules @mostajs/*
48
+ ├── lib/
49
+ │ ├── setup.ts # needsSetup() + runInstall()
50
+ │ ├── compose-uri.ts # Compose URI de connexion (mongo, pg, mysql...)
51
+ │ ├── db-test.ts # Test de connexion ephemere
52
+ │ ├── env-writer.ts # Ecriture/maj de .env.local
53
+ │ └── discover-modules.ts # Decouverte npm dynamique
54
+ ├── api/
55
+ │ ├── status.route.ts # Factory GET /api/setup/status
56
+ │ ├── test-db.route.ts # Factory POST /api/setup/test-db
57
+ │ ├── install.route.ts # Factory POST /api/setup/install
58
+ │ ├── detect-modules.route.ts # Factory GET /api/setup/detect-modules
59
+ │ └── install-modules.route.ts # Factory POST /api/setup/install-modules
60
+ ├── types/
61
+ │ └── index.ts # Tous les types TypeScript
62
+ └── index.ts # Barrel exports
63
+ ```
64
+
65
+ **Principe** : le package fournit des **factory functions** qui retournent des handlers `{ GET }` ou `{ POST }`. Votre app Next.js les expose en une ligne par route.
66
+
67
+ ---
68
+
69
+ ## 3. Didacticiel : integrer @mostajs/setup dans une nouvelle app
70
+
71
+ ### Prerequis
72
+
73
+ - Next.js 14+ (App Router)
74
+ - Node.js >= 18
75
+ - `@mostajs/orm` installe
76
+
77
+ ### Etape 1 — Installation des dependances
78
+
79
+ ```bash
80
+ npm install @mostajs/setup @mostajs/orm bcryptjs
81
+ npm install -D @types/bcryptjs
82
+ ```
83
+
84
+ ### Etape 2 — Configurer le setup (`lib/setup.ts`)
19
85
 
20
- ### 1. Create setup config
86
+ Ce fichier est le **pont** entre le package generique et votre application. Il definit :
87
+ - Comment compter les utilisateurs (pour `needsSetup`)
88
+ - Comment seeder les roles/permissions
89
+ - Comment creer l'admin
90
+ - Les seeds optionnels propres a votre app
21
91
 
22
92
  ```typescript
23
- import type { MostaSetupConfig } from '@mostajs/setup'
93
+ // src/lib/setup.ts
94
+ import {
95
+ needsSetup as _needsSetup,
96
+ runInstall as _runInstall,
97
+ } from '@mostajs/setup/lib/setup'
98
+ import type {
99
+ DialectType,
100
+ MostaSetupConfig,
101
+ SeedDefinition,
102
+ InstallConfig,
103
+ } from '@mostajs/setup'
24
104
 
25
- export const setupConfig: MostaSetupConfig = {
26
- appName: 'MyApp',
105
+ export type { DialectType }
106
+
107
+ // ─── needsSetup : verifie si la DB a 0 users ──────────────
108
+ export async function needsSetup(): Promise<boolean> {
109
+ return _needsSetup(async () => {
110
+ // Adaptez cette ligne a votre couche d'acces aux donnees
111
+ const { userRepo } = await import('@/dal/service')
112
+ const repo = await userRepo()
113
+ return repo.count()
114
+ })
115
+ }
116
+
117
+ // ─── Seeds optionnels (propres a votre app) ────────────────
118
+ const optionalSeeds: SeedDefinition[] = [
119
+ {
120
+ key: 'demoData',
121
+ label: 'Donnees de demonstration',
122
+ description: 'Quelques enregistrements pour tester',
123
+ run: async () => {
124
+ // Votre logique de seed
125
+ const { productRepo } = await import('@/dal/service')
126
+ const repo = await productRepo()
127
+ await repo.create({ name: 'Produit A', price: 1000 })
128
+ await repo.create({ name: 'Produit B', price: 2000 })
129
+ },
130
+ },
131
+ ]
132
+
133
+ // ─── Configuration du setup ────────────────────────────────
134
+ const SETUP_CONFIG: MostaSetupConfig = {
135
+ appName: 'Mon Application',
27
136
  defaultPort: 3000,
28
- seedRBAC: async () => { /* seed roles & permissions */ },
29
- createAdmin: async ({ email, hashedPassword, firstName, lastName }) => { /* create admin user */ },
137
+
138
+ // Callback pour seeder les roles et permissions
139
+ seedRBAC: async () => {
140
+ const { roleRepo, permissionRepo } = await import('@/dal/service')
141
+ const pRepo = await permissionRepo()
142
+ const rRepo = await roleRepo()
143
+
144
+ // Creer les permissions
145
+ const readPerm = await pRepo.upsert(
146
+ { name: 'read' },
147
+ { name: 'read', description: 'Lecture' },
148
+ )
149
+ const writePerm = await pRepo.upsert(
150
+ { name: 'write' },
151
+ { name: 'write', description: 'Ecriture' },
152
+ )
153
+
154
+ // Creer les roles avec permissions
155
+ await rRepo.upsert(
156
+ { name: 'admin' },
157
+ { name: 'admin', description: 'Administrateur', permissions: [readPerm.id, writePerm.id] },
158
+ )
159
+ await rRepo.upsert(
160
+ { name: 'viewer' },
161
+ { name: 'viewer', description: 'Lecteur', permissions: [readPerm.id] },
162
+ )
163
+ },
164
+
165
+ // Callback pour creer le premier administrateur
166
+ // Le mot de passe est DEJA hashe par le package (bcrypt, 12 rounds)
167
+ createAdmin: async ({ email, hashedPassword, firstName, lastName }) => {
168
+ const { userRepo, roleRepo } = await import('@/dal/service')
169
+ const uRepo = await userRepo()
170
+ const rRepo = await roleRepo()
171
+
172
+ const adminRole = await rRepo.findOne({ name: 'admin' })
173
+ await uRepo.upsert(
174
+ { email: email.toLowerCase() },
175
+ {
176
+ email: email.toLowerCase(),
177
+ password: hashedPassword,
178
+ firstName,
179
+ lastName,
180
+ roles: adminRole ? [adminRole.id] : [],
181
+ status: 'active',
182
+ },
183
+ )
184
+ },
185
+
186
+ optionalSeeds,
187
+ }
188
+
189
+ // ─── runInstall : expose au route handler ──────────────────
190
+ export async function runInstall(config: InstallConfig) {
191
+ return _runInstall(config, SETUP_CONFIG)
30
192
  }
31
193
  ```
32
194
 
33
- ### 2. API routes
195
+ **Points cles :**
196
+ - `needsSetup()` retourne `true` si 0 users en base → l'app doit s'installer
197
+ - `runInstall()` execute les 6 etapes : ecriture `.env.local`, connexion DB, seed RBAC, creation admin, seeds optionnels
198
+ - Le mot de passe admin est **automatiquement hashe** par le package (bcrypt, 12 rounds). Ne le hachez PAS vous-meme.
199
+ - `MOSTAJS_MODULES` est ecrit dans `.env.local` si `config.modules` est fourni
200
+
201
+ ### Etape 3 — Creer les routes API
202
+
203
+ Creez 5 fichiers sous `src/app/api/setup/` :
204
+
205
+ #### 3.1 — Status (GET)
34
206
 
35
207
  ```typescript
36
- // app/api/setup/status/route.ts
37
- import { createStatusHandler, needsSetup } from '@mostajs/setup'
38
- export const { GET } = createStatusHandler(() => needsSetup(countUsers))
208
+ // src/app/api/setup/status/route.ts
209
+ import { createStatusHandler } from '@mostajs/setup'
210
+ import { needsSetup } from '@/lib/setup'
39
211
 
40
- // app/api/setup/test-db/route.ts
212
+ export const { GET } = createStatusHandler(needsSetup)
213
+ ```
214
+
215
+ Reponse : `{ "needsSetup": true }` ou `{ "needsSetup": false }`
216
+
217
+ #### 3.2 — Test connexion DB (POST)
218
+
219
+ ```typescript
220
+ // src/app/api/setup/test-db/route.ts
41
221
  import { createTestDbHandler } from '@mostajs/setup'
42
- export const { POST } = createTestDbHandler(() => needsSetup(countUsers))
222
+ import { needsSetup } from '@/lib/setup'
223
+
224
+ export const { POST } = createTestDbHandler(needsSetup)
225
+ ```
226
+
227
+ Body attendu :
228
+ ```json
229
+ {
230
+ "dialect": "postgres",
231
+ "host": "localhost",
232
+ "port": 5432,
233
+ "name": "mydb",
234
+ "user": "postgres",
235
+ "password": "secret"
236
+ }
237
+ ```
238
+
239
+ Reponse : `{ "ok": true }` ou `{ "ok": false, "error": "..." }`
240
+
241
+ #### 3.3 — Installation (POST)
242
+
243
+ ```typescript
244
+ // src/app/api/setup/install/route.ts
245
+ import { NextRequest, NextResponse } from 'next/server'
246
+ import { needsSetup, runInstall } from '@/lib/setup'
247
+ import { z } from 'zod'
248
+
249
+ const ALL_DIALECTS = [
250
+ 'mongodb', 'sqlite', 'postgres', 'mysql', 'mariadb',
251
+ 'oracle', 'mssql', 'cockroachdb', 'db2', 'hana',
252
+ 'hsqldb', 'spanner', 'sybase',
253
+ ] as const
254
+
255
+ const installSchema = z.object({
256
+ dialect: z.enum(ALL_DIALECTS),
257
+ db: z.object({
258
+ host: z.string().default('localhost'),
259
+ port: z.number().int().min(0).max(65535).default(0),
260
+ name: z.string().min(1),
261
+ user: z.string().default(''),
262
+ password: z.string().default(''),
263
+ }),
264
+ admin: z.object({
265
+ email: z.string().email(),
266
+ password: z.string().min(6),
267
+ firstName: z.string().min(1),
268
+ lastName: z.string().min(1),
269
+ }),
270
+ seed: z.record(z.boolean()).optional(),
271
+ modules: z.array(z.string()).optional(),
272
+ })
273
+
274
+ export async function POST(req: NextRequest) {
275
+ const setupNeeded = await needsSetup()
276
+ if (!setupNeeded) {
277
+ return NextResponse.json(
278
+ { error: { code: 'FORBIDDEN', message: 'Installation deja effectuee' } },
279
+ { status: 403 },
280
+ )
281
+ }
282
+
283
+ const body = await req.json()
284
+ const parsed = installSchema.safeParse(body)
285
+ if (!parsed.success) {
286
+ return NextResponse.json(
287
+ { error: { code: 'VALIDATION_ERROR', details: parsed.error.flatten() } },
288
+ { status: 400 },
289
+ )
290
+ }
291
+
292
+ const result = await runInstall(parsed.data)
293
+ if (!result.ok) {
294
+ return NextResponse.json(
295
+ { error: { code: 'INSTALL_ERROR', message: result.error } },
296
+ { status: 500 },
297
+ )
298
+ }
299
+
300
+ return NextResponse.json({
301
+ data: { ok: true, needsRestart: result.needsRestart, seeded: result.seeded },
302
+ })
303
+ }
304
+ ```
305
+
306
+ #### 3.4 — Detection des modules (GET)
307
+
308
+ ```typescript
309
+ // src/app/api/setup/detect-modules/route.ts
310
+ import { createDetectModulesHandler } from '@mostajs/setup'
311
+
312
+ export const { GET } = createDetectModulesHandler()
313
+ ```
314
+
315
+ Reponse :
316
+ ```json
317
+ {
318
+ "modules": [
319
+ { "key": "orm", "packageName": "@mostajs/orm", "label": "ORM", "required": true, ... },
320
+ { "key": "auth", "packageName": "@mostajs/auth", ... },
321
+ { "key": "new-plugin", "discovered": true, "icon": "📦", ... }
322
+ ],
323
+ "installed": ["orm", "auth", "setup"]
324
+ }
325
+ ```
326
+
327
+ #### 3.5 — Installation des modules (POST)
328
+
329
+ ```typescript
330
+ // src/app/api/setup/install-modules/route.ts
331
+ import { createInstallModulesHandler } from '@mostajs/setup'
332
+ import { needsSetup } from '@/lib/setup'
333
+
334
+ export const { POST } = createInstallModulesHandler(needsSetup)
335
+ ```
336
+
337
+ Body : `{ "modules": ["orm", "auth", "audit", "rbac"] }`
338
+
339
+ Ce handler :
340
+ 1. Resout les dependances transitives (`rbac` → `auth` + `audit`)
341
+ 2. Installe via `npm install` (local `file:./packages/...` si present, sinon npm registry)
342
+ 3. Ecrit `MOSTAJS_MODULES=orm,auth,audit,rbac` dans `.env.local`
343
+
344
+ ### Etape 4 — Creer la page Setup (frontend)
345
+
346
+ Voici un exemple **minimal** de page setup. Adaptez le style a votre design system.
347
+
348
+ ```tsx
349
+ // src/app/setup/page.tsx
350
+ 'use client'
351
+
352
+ import { useState, useEffect } from 'react'
353
+ import { useRouter } from 'next/navigation'
354
+ import { resolveModuleDependencies } from '@mostajs/setup/data/module-definitions'
355
+ import type { ModuleDefinition } from '@mostajs/setup'
356
+
357
+ type Dialect = 'mongodb' | 'sqlite' | 'postgres' | 'mysql' | 'mariadb'
358
+ | 'oracle' | 'mssql' | 'cockroachdb' | 'db2' | 'hana'
359
+ | 'hsqldb' | 'spanner' | 'sybase'
360
+
361
+ export default function SetupPage() {
362
+ const router = useRouter()
363
+
364
+ // --- Etat du wizard ---
365
+ const [step, setStep] = useState(1) // 1=modules, 2=db, 3=admin, 4=install
366
+
367
+ // Modules
368
+ const [availableModules, setAvailableModules] = useState<ModuleDefinition[]>([])
369
+ const [selectedModules, setSelectedModules] = useState<string[]>([])
370
+ const [installedModules, setInstalledModules] = useState<string[]>([])
371
+
372
+ // DB
373
+ const [dialect, setDialect] = useState<Dialect>('mongodb')
374
+ const [dbConfig, setDbConfig] = useState({ host: 'localhost', port: 27017, name: 'mydb', user: '', password: '' })
375
+ const [dbOk, setDbOk] = useState(false)
376
+
377
+ // Admin
378
+ const [admin, setAdmin] = useState({ email: '', password: '', firstName: '', lastName: '' })
379
+
380
+ // Install
381
+ const [installing, setInstalling] = useState(false)
382
+ const [result, setResult] = useState<{ ok: boolean; error?: string } | null>(null)
383
+
384
+ // --- Charger les modules depuis l'API au montage ---
385
+ useEffect(() => {
386
+ fetch('/api/setup/detect-modules')
387
+ .then(r => r.json())
388
+ .then((data: { modules: ModuleDefinition[]; installed: string[] }) => {
389
+ setAvailableModules(data.modules || [])
390
+ setInstalledModules(data.installed || [])
391
+ // Pre-cocher les modules required + default + deja installes
392
+ const preChecked = new Set([
393
+ ...(data.modules || []).filter(m => m.required || m.default).map(m => m.key),
394
+ ...(data.installed || []),
395
+ ])
396
+ setSelectedModules(Array.from(preChecked))
397
+ })
398
+ .catch(() => {})
399
+ }, [])
400
+
401
+ // --- Toggle un module (avec resolution des dependances) ---
402
+ function toggleModule(key: string) {
403
+ const mod = availableModules.find(m => m.key === key)
404
+ if (!mod || mod.required) return
405
+
406
+ setSelectedModules(prev => {
407
+ if (prev.includes(key)) {
408
+ // Decochage : retirer aussi les modules qui dependent de celui-ci
409
+ const toRemove = new Set([key])
410
+ let changed = true
411
+ while (changed) {
412
+ changed = false
413
+ for (const m of availableModules) {
414
+ if (toRemove.has(m.key) || m.required) continue
415
+ if (m.dependsOn?.some(dep => toRemove.has(dep)) && prev.includes(m.key)) {
416
+ toRemove.add(m.key)
417
+ changed = true
418
+ }
419
+ }
420
+ }
421
+ return prev.filter(k => !toRemove.has(k))
422
+ } else {
423
+ // Cochage : ajouter les dependances transitives
424
+ return resolveModuleDependencies([...prev, key], availableModules)
425
+ }
426
+ })
427
+ }
428
+
429
+ // --- Tester la connexion DB ---
430
+ async function testDb() {
431
+ const res = await fetch('/api/setup/test-db', {
432
+ method: 'POST',
433
+ headers: { 'Content-Type': 'application/json' },
434
+ body: JSON.stringify({ dialect, ...dbConfig }),
435
+ })
436
+ const data = await res.json()
437
+ setDbOk(data.ok === true)
438
+ }
439
+
440
+ // --- Lancer l'installation ---
441
+ async function install() {
442
+ setInstalling(true)
443
+ setResult(null)
444
+
445
+ // 1. Installer les modules npm
446
+ const modRes = await fetch('/api/setup/install-modules', {
447
+ method: 'POST',
448
+ headers: { 'Content-Type': 'application/json' },
449
+ body: JSON.stringify({ modules: selectedModules }),
450
+ })
451
+ if (!modRes.ok) {
452
+ setResult({ ok: false, error: 'Erreur installation modules' })
453
+ setInstalling(false)
454
+ return
455
+ }
456
+
457
+ // 2. Configurer la DB + seeder
458
+ const res = await fetch('/api/setup/install', {
459
+ method: 'POST',
460
+ headers: { 'Content-Type': 'application/json' },
461
+ body: JSON.stringify({ dialect, db: dbConfig, admin, modules: selectedModules }),
462
+ })
463
+ const data = await res.json()
464
+ setResult(data.data || { ok: false, error: data.error?.message })
465
+ setInstalling(false)
466
+ }
467
+
468
+ return (
469
+ <div style={{ maxWidth: 600, margin: '50px auto', padding: 20 }}>
470
+ <h1>Setup — Mon Application</h1>
471
+
472
+ {/* ── Etape 1 : Modules ── */}
473
+ {step === 1 && (
474
+ <div>
475
+ <h2>1. Modules</h2>
476
+ {availableModules.map(mod => (
477
+ <label key={mod.key} style={{ display: 'block', margin: '8px 0' }}>
478
+ <input
479
+ type="checkbox"
480
+ checked={selectedModules.includes(mod.key)}
481
+ disabled={mod.required}
482
+ onChange={() => toggleModule(mod.key)}
483
+ />
484
+ {' '}{mod.icon} {mod.label}
485
+ {mod.required && <em> (requis)</em>}
486
+ {mod.discovered && <strong> NOUVEAU</strong>}
487
+ {installedModules.includes(mod.key) && <span> ✓</span>}
488
+ </label>
489
+ ))}
490
+ <button onClick={() => setStep(2)}>Suivant →</button>
491
+ </div>
492
+ )}
493
+
494
+ {/* ── Etape 2 : Base de donnees ── */}
495
+ {step === 2 && (
496
+ <div>
497
+ <h2>2. Base de donnees</h2>
498
+ <select value={dialect} onChange={e => setDialect(e.target.value as Dialect)}>
499
+ <option value="mongodb">MongoDB</option>
500
+ <option value="sqlite">SQLite</option>
501
+ <option value="postgres">PostgreSQL</option>
502
+ <option value="mysql">MySQL</option>
503
+ {/* ... autres dialectes ... */}
504
+ </select>
505
+
506
+ {dialect !== 'sqlite' && (
507
+ <>
508
+ <input placeholder="Host" value={dbConfig.host}
509
+ onChange={e => setDbConfig({ ...dbConfig, host: e.target.value })} />
510
+ <input placeholder="Port" type="number" value={dbConfig.port}
511
+ onChange={e => setDbConfig({ ...dbConfig, port: +e.target.value })} />
512
+ <input placeholder="User" value={dbConfig.user}
513
+ onChange={e => setDbConfig({ ...dbConfig, user: e.target.value })} />
514
+ <input placeholder="Password" type="password" value={dbConfig.password}
515
+ onChange={e => setDbConfig({ ...dbConfig, password: e.target.value })} />
516
+ </>
517
+ )}
518
+ <input placeholder="Nom de la base" value={dbConfig.name}
519
+ onChange={e => setDbConfig({ ...dbConfig, name: e.target.value })} />
520
+
521
+ <button onClick={testDb}>Tester la connexion</button>
522
+ {dbOk && <span style={{ color: 'green' }}> ✓ Connexion OK</span>}
523
+
524
+ <br />
525
+ <button onClick={() => setStep(1)}>← Retour</button>
526
+ <button onClick={() => setStep(3)} disabled={dialect !== 'sqlite' && !dbOk}>
527
+ Suivant →
528
+ </button>
529
+ </div>
530
+ )}
531
+
532
+ {/* ── Etape 3 : Administrateur ── */}
533
+ {step === 3 && (
534
+ <div>
535
+ <h2>3. Administrateur</h2>
536
+ <input placeholder="Prenom" value={admin.firstName}
537
+ onChange={e => setAdmin({ ...admin, firstName: e.target.value })} />
538
+ <input placeholder="Nom" value={admin.lastName}
539
+ onChange={e => setAdmin({ ...admin, lastName: e.target.value })} />
540
+ <input placeholder="Email" type="email" value={admin.email}
541
+ onChange={e => setAdmin({ ...admin, email: e.target.value })} />
542
+ <input placeholder="Mot de passe (min 6)" type="password" value={admin.password}
543
+ onChange={e => setAdmin({ ...admin, password: e.target.value })} />
544
+
545
+ <br />
546
+ <button onClick={() => setStep(2)}>← Retour</button>
547
+ <button onClick={() => setStep(4)}>Suivant →</button>
548
+ </div>
549
+ )}
550
+
551
+ {/* ── Etape 4 : Installation ── */}
552
+ {step === 4 && (
553
+ <div>
554
+ <h2>4. Recapitulatif</h2>
555
+ <p><strong>DB :</strong> {dialect} — {dbConfig.name}</p>
556
+ <p><strong>Admin :</strong> {admin.email}</p>
557
+ <p><strong>Modules :</strong> {selectedModules.join(', ')}</p>
558
+
559
+ <button onClick={install} disabled={installing}>
560
+ {installing ? 'Installation en cours...' : 'Installer'}
561
+ </button>
562
+
563
+ {result?.ok && (
564
+ <div style={{ color: 'green', marginTop: 16 }}>
565
+ ✓ Installation terminee !
566
+ <br />
567
+ <button onClick={() => router.push('/login')}>Aller au login →</button>
568
+ </div>
569
+ )}
570
+ {result && !result.ok && (
571
+ <div style={{ color: 'red', marginTop: 16 }}>
572
+ ✗ Erreur : {result.error}
573
+ </div>
574
+ )}
575
+ </div>
576
+ )}
577
+ </div>
578
+ )
579
+ }
580
+ ```
581
+
582
+ ### Etape 5 — Middleware : rediriger vers /setup
583
+
584
+ ```typescript
585
+ // src/middleware.ts
586
+ import { NextResponse } from 'next/server'
587
+ import type { NextRequest } from 'next/server'
588
+
589
+ export async function middleware(req: NextRequest) {
590
+ const { pathname } = req.nextUrl
591
+
592
+ // Laisser passer les routes publiques
593
+ if (
594
+ pathname.startsWith('/setup') ||
595
+ pathname.startsWith('/api/setup') ||
596
+ pathname.startsWith('/api/auth') ||
597
+ pathname.startsWith('/_next')
598
+ ) {
599
+ return NextResponse.next()
600
+ }
601
+
602
+ // Verifier si l'app a besoin du setup
603
+ try {
604
+ const res = await fetch(new URL('/api/setup/status', req.url))
605
+ const data = await res.json()
606
+ if (data.needsSetup) {
607
+ return NextResponse.redirect(new URL('/setup', req.url))
608
+ }
609
+ } catch {
610
+ // Si la DB n'est pas accessible, rediriger vers setup
611
+ return NextResponse.redirect(new URL('/setup', req.url))
612
+ }
613
+
614
+ return NextResponse.next()
615
+ }
616
+
617
+ export const config = {
618
+ matcher: ['/((?!_next|favicon.ico|icons|manifest.json).*)'],
619
+ }
620
+ ```
621
+
622
+ ### Etape 6 — Tester le flux complet
623
+
624
+ ```bash
625
+ # 1. Demarrer le serveur
626
+ npm run dev
627
+
628
+ # 2. Ouvrir http://localhost:3000
629
+ # → Redirige automatiquement vers /setup
43
630
 
44
- // app/api/setup/install/route.ts
45
- import { createInstallHandler } from '@mostajs/setup'
46
- export const { POST } = createInstallHandler(() => needsSetup(countUsers), setupConfig)
631
+ # 3. Suivre le wizard :
632
+ # - Choisir les modules
633
+ # - Selectionner le dialecte (ex: MongoDB, SQLite, Postgres...)
634
+ # - Tester la connexion
635
+ # - Renseigner l'admin
636
+ # - Lancer l'installation
637
+
638
+ # 4. Verifier les resultats :
639
+ # - .env.local contient DB_DIALECT, SGBD_URI, MOSTAJS_MODULES
640
+ # - La DB contient les roles, permissions et l'admin
641
+ # - /api/setup/status retourne { needsSetup: false }
47
642
  ```
48
643
 
49
- ## Features
644
+ ---
645
+
646
+ ## 4. API Reference
647
+
648
+ ### Fonctions principales
50
649
 
51
- - **13 database dialects** all dialects supported by @mostajs/orm
52
- - **Connection testing** — test DB connection before saving
53
- - **URI composition** auto-build connection URI from form fields
54
- - **.env.local writer** persist DB config to disk
55
- - **Seed runner** RBAC + admin user creation
56
- - **Dialect metadata** names, icons, default ports, categories
650
+ | Export | Signature | Description |
651
+ |--------|-----------|-------------|
652
+ | `needsSetup` | `(countFn: () => Promise<number>) => Promise<boolean>` | Retourne `true` si 0 users en base |
653
+ | `runInstall` | `(config: InstallConfig, setup: MostaSetupConfig) => Promise<Result>` | Flux complet : env → DB seed → admin |
654
+ | `testDbConnection` | `(config: DbTestConfig) => Promise<{ ok, error? }>` | Test ephemere (ne modifie pas la connexion globale) |
655
+ | `composeDbUri` | `(dialect: DialectType, db: DbConfig) => string` | Compose l'URI de connexion |
656
+ | `writeEnvLocal` | `(options: EnvWriterOptions) => Promise<boolean>` | Ecrit `.env.local`, retourne `true` si dialect change |
57
657
 
58
- ## API Reference
658
+ ### Factories API routes
659
+
660
+ | Export | Route | Methode | Description |
661
+ |--------|-------|---------|-------------|
662
+ | `createStatusHandler(needsSetup)` | `/api/setup/status` | GET | Retourne `{ needsSetup: boolean }` |
663
+ | `createTestDbHandler(needsSetup)` | `/api/setup/test-db` | POST | Teste la connexion DB |
664
+ | `createInstallHandler(needsSetup, config)` | `/api/setup/install` | POST | Lance l'installation complete |
665
+ | `createDetectModulesHandler()` | `/api/setup/detect-modules` | GET | Liste modules (statiques + npm) + installes |
666
+ | `createInstallModulesHandler(needsSetup)` | `/api/setup/install-modules` | POST | Installe les modules npm selectionnes |
667
+
668
+ ### Data exports
59
669
 
60
670
  | Export | Description |
61
671
  |--------|-------------|
62
- | `needsSetup(countFn)` | Check if app needs initial setup |
63
- | `runInstall(config)` | Full installation flow |
64
- | `testDbConnection(config)` | Test DB connection |
65
- | `composeDbUri(config)` | Build connection URI |
66
- | `writeEnvLocal(vars)` | Write/update .env.local |
67
- | `DIALECT_INFO` / `ALL_DIALECTS` | Dialect metadata |
68
- | `createStatusHandler()` | GET /setup/status factory |
69
- | `createTestDbHandler()` | POST /setup/test-db factory |
70
- | `createInstallHandler()` | POST /setup/install factory |
672
+ | `DIALECT_INFO` | `Record<DialectType, DialectInfo>` metadata de chaque SGBD |
673
+ | `ALL_DIALECTS` | `DialectType[]` liste des 13 dialectes |
674
+ | `MODULES` | `ModuleDefinition[]` liste statique des 7 modules connus |
675
+ | `resolveModuleDependencies(selected, modules?)` | Resout les dependances transitives |
676
+ | `discoverNpmModules()` | Decouvre les packages `@mostajs/*` sur npm |
677
+
678
+ ### Types
679
+
680
+ ```typescript
681
+ interface InstallConfig {
682
+ dialect: DialectType
683
+ db: DbConfig
684
+ admin: { email: string; password: string; firstName: string; lastName: string }
685
+ seed?: SeedOptions // { activities: true, demoData: false, ... }
686
+ modules?: string[] // ['orm', 'auth', 'rbac'] → ecrit MOSTAJS_MODULES
687
+ }
688
+
689
+ interface MostaSetupConfig {
690
+ appName: string
691
+ defaultPort?: number
692
+ seedRBAC?: () => Promise<void>
693
+ createAdmin?: (admin: { email: string; hashedPassword: string; firstName: string; lastName: string }) => Promise<void>
694
+ optionalSeeds?: SeedDefinition[]
695
+ extraEnvVars?: Record<string, string>
696
+ }
697
+
698
+ interface SeedDefinition {
699
+ key: string
700
+ label: string
701
+ description: string
702
+ icon?: string
703
+ default?: boolean
704
+ run: (repos: any) => Promise<void>
705
+ }
706
+
707
+ interface ModuleDefinition {
708
+ key: string
709
+ packageName: string // '@mostajs/auth'
710
+ localDir?: string // 'mosta-auth' (sous packages/)
711
+ label: string
712
+ description: string
713
+ icon: string
714
+ required?: boolean
715
+ default?: boolean
716
+ dependsOn?: string[]
717
+ discovered?: boolean // true si trouve via npm search
718
+ }
719
+ ```
720
+
721
+ ---
722
+
723
+ ## 5. Les 13 dialectes supportes
724
+
725
+ | Dialecte | Icone | Port | Categorie | Driver |
726
+ |----------|-------|------|-----------|--------|
727
+ | MongoDB | 🍃 | 27017 | document | mongoose |
728
+ | SQLite | 📁 | — | file | better-sqlite3 |
729
+ | PostgreSQL | 🐘 | 5432 | sql | pg |
730
+ | MySQL | 🐬 | 3306 | sql | mysql2 |
731
+ | MariaDB | 🦭 | 3306 | sql | mariadb |
732
+ | Oracle | 🔴 | 1521 | enterprise | oracledb |
733
+ | SQL Server | 🟦 | 1433 | enterprise | tedious |
734
+ | CockroachDB | 🪳 | 26257 | distributed | pg |
735
+ | IBM DB2 | 🏢 | 50000 | enterprise | ibm_db |
736
+ | SAP HANA | 💎 | 39013 | enterprise | @sap/hana-client |
737
+ | HyperSQL | ⚡ | 9001 | legacy | — |
738
+ | Cloud Spanner | ☁️ | — | distributed | @google-cloud/spanner |
739
+ | Sybase ASE | 🔷 | 5000 | legacy | sybase |
740
+
741
+ Les URI sont composees automatiquement par `composeDbUri()` :
742
+
743
+ ```
744
+ mongodb://user:pass@host:27017/mydb
745
+ postgresql://user:pass@host:5432/mydb
746
+ mysql://user:pass@host:3306/mydb
747
+ ./data/mydb.db (SQLite)
748
+ spanner://projects/my-project (Cloud Spanner)
749
+ ```
750
+
751
+ ---
752
+
753
+ ## 6. Systeme de modules
754
+
755
+ ### Catalogue des 7 modules connus
756
+
757
+ | Module | Package | Requis | Depend de | Standalone | Description |
758
+ |--------|---------|--------|-----------|------------|-------------|
759
+ | **orm** | `@mostajs/orm` | oui | — | — | ORM multi-dialecte (13 SGBD), pattern Hibernate |
760
+ | **auth** | `@mostajs/auth` | oui | orm | non | NextAuth, sessions, hashage mots de passe |
761
+ | **audit** | `@mostajs/audit` | non | orm | non | Journalisation des actions, tracabilite |
762
+ | **rbac** | `@mostajs/rbac` | non | auth, audit | non | Roles, permissions, matrice RBAC |
763
+ | **settings** | `@mostajs/settings` | non | orm | non | Parametres cle-valeur, formulaire auto, provider React |
764
+ | **face** | `@mostajs/face` | non | **aucune** | **oui** | Detection de visage, descripteurs, matching 1:N |
765
+ | **setup** | `@mostajs/setup` | oui | orm | non | Wizard d'installation, test DB, seed runner |
766
+
767
+ ### Module standalone : @mostajs/face
768
+
769
+ `@mostajs/face` est **100% independant** — il n'importe aucun package `@mostajs/*` et peut etre utilise dans n'importe quelle application React >= 18 sans `@mostajs/orm` ni base de donnees.
770
+
771
+ **Dependance unique** : `@vladmandic/face-api` (reconnaissance faciale TensorFlow.js)
772
+
773
+ ```bash
774
+ npm install @mostajs/face
775
+ ```
776
+
777
+ ```tsx
778
+ import { useCamera, useFaceDetection, compareFaces } from '@mostajs/face'
779
+
780
+ // Hooks React pour camera et detection
781
+ const { videoRef, start, stop } = useCamera()
782
+ const { detect, result } = useFaceDetection()
783
+
784
+ // API bas niveau
785
+ import { loadModels, detectFace, extractDescriptor } from '@mostajs/face'
786
+ import { findMatch, descriptorToArray, arrayToDescriptor } from '@mostajs/face'
787
+ ```
788
+
789
+ Exports : `loadModels`, `detectFace`, `detectAllFaces`, `extractDescriptor`, `compareFaces`, `findMatch`, `findAllMatches`, `descriptorToArray`, `arrayToDescriptor`, `isValidDescriptor`, `drawDetection`, `useCamera`, `useFaceDetection`.
790
+
791
+ ### Graphe de dependances
792
+
793
+ ```
794
+ ┌──────────┐
795
+ │ orm (R) │ R = requis
796
+ └────┬─────┘
797
+ ┌────────┼────────┬──────────┐
798
+ v v v v
799
+ ┌──────────┐ ┌──────┐ ┌──────────┐ ┌───────┐
800
+ │ auth (R) │ │audit │ │ settings │ │setup(R)│
801
+ └────┬─────┘ └──┬───┘ └──────────┘ └───────┘
802
+ │ │
803
+ v v
804
+ ┌──────────────────┐
805
+ │ rbac │
806
+ └──────────────────┘
807
+
808
+ ┌──────────────────────────┐
809
+ │ face (100% standalone) │ ← aucune dependance @mostajs
810
+ └──────────────────────────┘
811
+ ```
812
+
813
+ ### Liste statique vs decouverte dynamique
814
+
815
+ Le package maintient une **liste statique** des 7 modules ci-dessus avec metadata riches (required, dependsOn, icon, description).
816
+
817
+ Au runtime, `GET /api/setup/detect-modules` interroge aussi **npm** (`npm search @mostajs --json`) pour trouver des packages publies apres le deploiement. Ces modules decouverts sont ajoutes avec `discovered: true` et l'icone 📦.
818
+
819
+ ### Resolution des dependances
820
+
821
+ ```typescript
822
+ import { resolveModuleDependencies } from '@mostajs/setup'
823
+
824
+ resolveModuleDependencies(['rbac'])
825
+ // → ['rbac', 'auth', 'audit', 'orm', 'setup']
826
+ // (rbac depend de auth + audit, auth depend de orm, setup est requis)
827
+
828
+ resolveModuleDependencies(['face'])
829
+ // → ['face', 'orm', 'auth', 'setup']
830
+ // (face n'a pas de dependance @mostajs, mais orm/auth/setup sont requis)
831
+ ```
832
+
833
+ ### Installation hybride (local + npm)
834
+
835
+ Le handler `install-modules` utilise une strategie hybride :
836
+
837
+ 1. **Skip** : si deja dans `node_modules/@mostajs/xxx` → pas de `npm install` (evite hot-reload)
838
+ 2. **Local** : si `packages/mosta-xxx/` existe → `npm install file:./packages/mosta-xxx`
839
+ 3. **npm registry** : sinon → `npm install @mostajs/xxx`
840
+
841
+ Cela evite les 404 npm pour les packages non encore publies et les hot-reloads Next.js inutiles.
842
+
843
+ ---
844
+
845
+ ## 7. Exemples avances
846
+
847
+ ### Utiliser runInstall sans le wizard UI
848
+
849
+ ```typescript
850
+ import { runInstall } from '@mostajs/setup'
851
+ import { setupConfig } from './my-setup-config'
852
+
853
+ const result = await runInstall(
854
+ {
855
+ dialect: 'postgres',
856
+ db: { host: 'localhost', port: 5432, name: 'mydb', user: 'pg', password: 'secret' },
857
+ admin: { email: 'admin@example.com', password: 'Admin@123', firstName: 'Admin', lastName: 'User' },
858
+ seed: { demoData: true },
859
+ modules: ['orm', 'auth', 'rbac'],
860
+ },
861
+ setupConfig,
862
+ )
863
+
864
+ console.log(result)
865
+ // { ok: true, needsRestart: false, seeded: ['categories', 'permissions', 'roles', 'admin', 'demoData'] }
866
+ ```
867
+
868
+ ### Ajouter des variables d'environnement personnalisees
869
+
870
+ ```typescript
871
+ const config: MostaSetupConfig = {
872
+ appName: 'MyApp',
873
+ extraEnvVars: {
874
+ SMTP_HOST: 'smtp.example.com',
875
+ STRIPE_KEY: 'sk_test_...',
876
+ },
877
+ }
878
+ // → .env.local contiendra aussi SMTP_HOST et STRIPE_KEY
879
+ ```
880
+
881
+ ### Tester la connexion DB programmatiquement
882
+
883
+ ```typescript
884
+ import { testDbConnection } from '@mostajs/setup'
885
+
886
+ const result = await testDbConnection({
887
+ dialect: 'mongodb',
888
+ host: 'localhost',
889
+ port: 27017,
890
+ name: 'testdb',
891
+ user: '',
892
+ password: '',
893
+ })
894
+ // { ok: true } ou { ok: false, error: 'Connection refused' }
895
+ ```
896
+
897
+ ### Composer une URI manuellement
898
+
899
+ ```typescript
900
+ import { composeDbUri } from '@mostajs/setup'
901
+
902
+ composeDbUri('postgres', { host: 'db.example.com', port: 5432, name: 'prod', user: 'app', password: 's3cret' })
903
+ // → 'postgresql://app:s3cret@db.example.com:5432/prod'
904
+
905
+ composeDbUri('sqlite', { host: '', port: 0, name: 'myapp', user: '', password: '' })
906
+ // → './data/myapp.db'
907
+ ```
908
+
909
+ ### Ecrire .env.local directement
910
+
911
+ ```typescript
912
+ import { writeEnvLocal } from '@mostajs/setup'
913
+
914
+ const dialectChanged = await writeEnvLocal({
915
+ dialect: 'mongodb',
916
+ uri: 'mongodb://localhost:27017/mydb',
917
+ port: 3000,
918
+ extraVars: { MOSTAJS_MODULES: 'orm,auth,rbac' },
919
+ })
920
+ // dialectChanged = true si le dialecte a change (necessite un redemarrage)
921
+ ```
922
+
923
+ ---
924
+
925
+ ## 8. FAQ / Troubleshooting
926
+
927
+ ### L'installation tourne en boucle (GET /setup se repete)
928
+
929
+ **Cause** : `npm install` modifie `package.json` / `node_modules`, ce qui declenche un hot-reload Next.js et reinitialise le state React.
930
+
931
+ **Solution** : Le handler `install-modules` **skip les packages deja installes** dans `node_modules/`. Si le probleme persiste, ajoutez une persistence du state wizard dans `sessionStorage` (voir l'implementation de SecuAccess Pro).
932
+
933
+ ### E11000 duplicate key error (MongoDB)
934
+
935
+ **Cause** : Un champ `unique: true` sans `required: true` dans le schema. MongoDB indexe les `null` et refuse le doublon.
936
+
937
+ **Solution** : Ajoutez `sparse: true` au champ dans votre `EntitySchema`. Cela fonctionne avec MongoDB (index sparse) et est ignore sans risque par les dialectes SQL.
938
+
939
+ ```typescript
940
+ clientNumber: { type: 'string', unique: true, sparse: true }
941
+ ```
942
+
943
+ ### JSON.parse: unexpected character at line 1 column 1
944
+
945
+ **Cause** : La reponse de l'API est du HTML (page 404 Next.js) au lieu de JSON, typiquement pendant un hot-reload du serveur.
946
+
947
+ **Solution** : Utilisez un `safeJson()` helper dans le frontend :
948
+
949
+ ```typescript
950
+ async function safeJson(res: Response) {
951
+ try { return JSON.parse(await res.text()) }
952
+ catch { return null }
953
+ }
954
+ ```
955
+
956
+ ### npm search timeout (pas d'internet)
957
+
958
+ Le handler `detect-modules` a un timeout de 10 secondes sur `npm search`. En cas d'echec (offline, timeout), il retourne la **liste statique** des 7 modules connus. Aucune action necessaire.
959
+
960
+ ### Comment ajouter un nouveau module a la liste statique ?
961
+
962
+ Editez `packages/mosta-setup/data/module-definitions.ts` et ajoutez une entree a `MODULES` :
963
+
964
+ ```typescript
965
+ {
966
+ key: 'notifications',
967
+ packageName: '@mostajs/notifications',
968
+ localDir: 'mosta-notifications', // si disponible localement
969
+ label: 'Notifications',
970
+ description: 'Push, email, SMS',
971
+ icon: '🔔',
972
+ default: false,
973
+ dependsOn: ['orm'],
974
+ }
975
+ ```
976
+
977
+ Puis recompilez : `cd packages/mosta-setup && npx tsc`
978
+
979
+ ---
71
980
 
72
981
  ## Related Packages
73
982
 
74
- - [@mostajs/orm](https://www.npmjs.com/package/@mostajs/orm) Multi-dialect ORM (required)
75
- - [@mostajs/auth](https://www.npmjs.com/package/@mostajs/auth) — Authentication (for RBAC seeding)
983
+ | Package | Depend de orm | Standalone | Description |
984
+ |---------|:---:|:---:|-------------|
985
+ | [@mostajs/orm](https://www.npmjs.com/package/@mostajs/orm) | — | — | Multi-dialect ORM, 13 SGBD (requis) |
986
+ | [@mostajs/auth](https://www.npmjs.com/package/@mostajs/auth) | oui | non | Authentication NextAuth, sessions |
987
+ | [@mostajs/audit](https://www.npmjs.com/package/@mostajs/audit) | oui | non | Audit logging, tracabilite |
988
+ | [@mostajs/rbac](https://www.npmjs.com/package/@mostajs/rbac) | oui | non | Roles & Permissions RBAC |
989
+ | [@mostajs/settings](https://www.npmjs.com/package/@mostajs/settings) | oui | non | Parametres cle-valeur |
990
+ | [@mostajs/face](https://www.npmjs.com/package/@mostajs/face) | **non** | **oui** | Reconnaissance faciale (independant) |
76
991
 
77
992
  ## License
78
993
 
79
- MIT — © 2025 Dr Hamid MADANI <drmdh@msn.com>
994
+ MIT — (c) 2025 Dr Hamid MADANI <drmdh@msn.com>