Skip to content

Guide développeur

Ce guide s'adresse aux développeurs qui travaillent sur le code source de GovernIA. Il couvre le setup local, l'architecture du monorepo, les patterns de code et le workflow de déploiement.


Setup local

Prérequis

OutilVersionUsage
Node.js20 LTSAPI, App, Backoffice
PostgreSQL14+Base de données
GitContrôle de version
Docker (optionnel)24+Environnement conteneurisé

1. Cloner le dépôt

bash
git clone https://github.com/nashvilleboy2019-art/governia.git
cd governia

2. Installer les dépendances

Chaque package est indépendant — npm install doit être exécuté dans chaque dossier :

bash
cd api && npm install && cd ..
cd app && npm install && cd ..
cd backoffice && npm install && cd ..

3. Configurer les variables d'environnement

api/.env (variables minimales)

env
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=governia_dev

JWT_SECRET=dev_secret_longue_chaine_ici
JWT_EXPIRES_IN=7d

APP_FRONTEND_URL=http://localhost:3001
BO_FRONTEND_URL=http://localhost:3002

NODE_ENV=development
PORT=4000

Toutes les intégrations (Agents IA, Stockage S3, Stripe, Jira) se configurent depuis Backoffice → Paramètres ou depuis l'interface tenant. Aucune variable .env supplémentaire n'est nécessaire pour démarrer l'API.

app/.env

env
NEXT_PUBLIC_API_URL=http://localhost:4000

backoffice/.env

env
NEXT_PUBLIC_API_URL=http://localhost:4000

4. Démarrer les services

bash
# Terminal 1 — API avec hot-reload
cd api && npm run start:dev

# Terminal 2 — App client
cd app && npm run dev

# Terminal 3 — Backoffice
cd backoffice && npm run dev

5. Vérifier l'installation

bash
# Santé de l'API
curl http://localhost:4000/health

# L'API répond aussi sur :
# http://localhost:4000/admin/*  (backoffice)
# http://localhost:4000/client/* (app)
# http://localhost:4000/auth/*   (public)

Architecture

Monorepo

governia/
├── api/                  NestJS 11 — backend REST
│   └── src/
│       ├── auth/
│       ├── users/
│       ├── missions/
│       ├── audits/
│       └── ...           32 modules au total
├── app/                  Next.js 16 — portail client
│   └── app/
│       ├── (auth)/       Routes publiques (login, invite)
│       └── (client)/
│           ├── _utils/   authFetch, apiFetchJson
│           └── suite/    Workspace principal
│               ├── audit-ia/
│               ├── audit-reglementaire/
│               ├── audits-techniques/
│               ├── audit-rgpd/
│               ├── analytics/
│               ├── missions/
│               └── ...
├── backoffice/           Next.js 16 — interface admin
│   ├── app/
│   │   ├── (auth)/
│   │   └── (app)/        Pages admin protégées
│   ├── context/
│   │   └── AuthContext.tsx
│   └── lib/
│       └── api.ts
└── docs/                 VitePress

Routes API

PréfixeGuardUsage
/auth/*Aucun (public)Connexion, inscription, acceptation d'invitation
/client/*JwtAuthGuardEndpoints portail client
/admin/*JwtAuthGuard + AdminGuardEndpoints backoffice

Multi-tenancy

Principe

  • Isolation par colonne tenant_id dans toutes les tables métier
  • Chaque requête client transporte le header X-Tenant-Id
  • Le header prend la priorité sur le tenantId du JWT

Pattern d'extraction du contexte

Dans les controllers client :

typescript
private ctx(user: any, req: Request): ClientCtx {
  const hdrTenantId = this.parseHeaderTenantId(req);  // lit 'x-tenant-id'
  const tRaw = Number.isFinite(hdrTenantId) && hdrTenantId > 0
    ? hdrTenantId
    : (user?.tenantId ?? user?.tenant_id ?? null);
  return {
    userId: Number(user?.id ?? user?.sub),
    tenantId: tRaw == null ? null : Number(tRaw),
    role: user?.role ?? null,
  };
}

Les controllers admin n'ont pas de restriction tenant — ils opèrent sur toutes les données.


Auth et guards

JWT payload

typescript
{
  sub: number,       // userId
  email: string,
  role: string,      // 'ADMIN' | 'MANAGER_BU' | 'AUDITOR' | 'RESP_CLIENT' | 'OPERATOR'
  tenantId?: number | null,
  tenantType?: string | null,
}

Décorer un controller

typescript
// Route authentifiée
@UseGuards(JwtAuthGuard)
@Get('something')
async getSomething(@CurrentUser() user: any) { ... }

// Admin uniquement (les deux guards sont requis — AdminGuard n'est pas implicite)
@UseGuards(JwtAuthGuard, AdminGuard)
@Get('admin/something')
async adminSomething() { ... }

// Avec vérification billing
@UseGuards(JwtAuthGuard, LimitGuard)
@Post('client/generate')
async generate() { ... }

LimitGuard

  • Lit le plan Stripe du tenant
  • Ajoute X-Usage-Warning: approaching-limit à >90% d'usage
  • Lève ForbiddenException à >110%

Ajouter un module API

Structure standard

api/src/<feature>/
  ├── <feature>.entity.ts
  ├── <feature>.service.ts
  ├── <feature>.controller.ts
  ├── <feature>.module.ts
  └── dto/
      ├── create-<feature>.dto.ts
      └── update-<feature>.dto.ts

Exemple minimal

entity

typescript
@Entity('features')
export class Feature {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  tenant_id: number;
}

service

typescript
@Injectable()
export class FeatureService {
  constructor(
    @InjectRepository(Feature)
    private repo: Repository<Feature>,
  ) {}

  findAll(tenantId: number) {
    return this.repo.find({ where: { tenant_id: tenantId } });
  }
}

controller

typescript
@UseGuards(JwtAuthGuard)
@Controller('client/features')
export class FeatureController {
  constructor(private svc: FeatureService) {}

  @Get()
  list(@CurrentUser() user: any, @Req() req: Request) {
    const { tenantId } = this.ctx(user, req);
    return this.svc.findAll(tenantId);
  }
}

module

typescript
@Module({
  imports: [TypeOrmModule.forFeature([Feature])],
  controllers: [FeatureController],
  providers: [FeatureService],
})
export class FeatureModule {}

Puis importer dans AppModule :

typescript
// api/src/app.module.ts
imports: [..., FeatureModule]

Ajouter une page App

Pattern de page client

tsx
'use client';

export default function MyPage() {
  const router = useRouter();
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let alive = true;
    const load = async () => {
      try {
        const result = await apiFetchJson<MyType>('/client/my-endpoint');
        if (alive) setData(result);
      } catch (e: any) {
        if (e?.status === 401) router.replace('/login');
      } finally {
        if (alive) setLoading(false);
      }
    };
    load();
    return () => { alive = false; };
  }, []);

  if (loading) return <div>Chargement...</div>;
  if (!data) return <div>Erreur</div>;
  return <div>{/* contenu */}</div>;
}

Helpers API client (app)

Toujours passer par app/app/(client)/_utils/authFetch.ts :

typescript
// Lecture de token avec fallbacks
getAuthToken()

// Fetch avec Authorization + X-Tenant-Id automatiques
authFetch('/client/endpoint', { method: 'POST', body: JSON.stringify(payload) })

// Fetch avec parsing JSON + gestion 204
apiFetchJson<T>('/client/endpoint')

Ne jamais lire localStorage directement pour le token ou le tenantId.

Helper API client (backoffice)

typescript
// backoffice/lib/api.ts
import { fetchApi } from '@/lib/api';

const data = await fetchApi('/admin/endpoint');

Base de données

TypeORM

  • synchronize: true en développement — les tables sont créées/modifiées automatiquement
  • En production : migrations explicites (les entities ne doivent pas casser les données existantes)

Requêtes SQL brutes

Pour les queries complexes multi-jointures (rapports, analytics) :

typescript
const rows = await this.dataSource.query(`
  SELECT
    a.id         AS "auditId",
    a.title      AS "auditTitle",
    COUNT(r.id)  AS "recommendationCount"
  FROM audits a
  LEFT JOIN recommendations r ON r.audit_id = a.id
  WHERE a.tenant_id = $1
  GROUP BY a.id, a.title
`, [tenantId]);

Utiliser $1, $2... comme placeholders. Retourner des colonnes avec AS "camelCaseName".

Transactions

typescript
await this.dataSource.transaction(async (em) => {
  const auditRepo = em.getRepository(Audit);
  const assignRepo = em.getRepository(Assignment);

  const audit = auditRepo.create({ ... });
  await auditRepo.save(audit);

  const assign = assignRepo.create({ auditId: audit.id, ... });
  await assignRepo.save(assign);
});

Tests

bash
cd api

npm run test        # Jest — tests unitaires
npm run test:watch  # Mode watch
npm run test:cov    # Rapport de couverture HTML (coverage/)
npm run test:e2e    # Tests end-to-end

npm run lint        # ESLint --fix

Déploiement

Branches

BrancheCibleDéclencheur
devVPS devPush + git pull + restart sur le VPS dev
mainVPS prodPR dev→main + merge + git pull + restart sur le VPS prod

Commandes VPS

bash
git pull origin main

# Rebuild et restart
docker compose -f docker-compose.prod.yml up -d --build api
docker compose -f docker-compose.prod.yml up -d --build backoffice

# App client — vider cache Next.js
docker volume rm app_next_cache bo_next_cache
docker compose -f docker-compose.prod.yml up -d

Nommer un service dans Docker Compose

ServiceRemarque
clientC'est le portail client (pas app)
backofficeBackoffice admin
apiBackend NestJS
dbPostgreSQL

Conventions de code

API

  • Tous les controllers protégés déclarent @UseGuards(JwtAuthGuard) — AdminGuard n'est jamais implicite
  • Les endpoints admin sont préfixés /admin/, les endpoints client /client/
  • La méthode ctx(user, req) extrait { userId, tenantId, role } depuis le JWT + header
  • Raw SQL préféré à QueryBuilder pour les queries de reporting (lisibilité, performant)

App/Backoffice

  • L'app client utilise apiFetchJson / authFetch — jamais de fetch direct
  • Le backoffice utilise fetchApi depuis lib/api.ts — jamais de fetch direct
  • Chaque page vérifie le token en début de useEffect et redirige sur /login si absent
  • Utiliser un flag alive ou isMountedRef pour éviter les setState après démontage

Style (backoffice)

Glassmorphism sombre :

rounded-3xl border border-white/10 bg-slate-950/70 p-5
shadow-[0_20px_60px_rgba(0,0,0,0.35)] backdrop-blur-xl

Badges de type : bleu pour CLIENT, violet pour CABINET. Badges de statut : émeraude pour ACTIVE, ambre pour PROSPECT.

GovernIA — Plateforme de gouvernance IA et conformité réglementaire