Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Optional AI assistant
OPENAI_API_KEY=
OPENAI_MODEL=gpt-5.6

# Optional Wave checkout
WAVE_API_KEY=
WAVE_SIGNING_SECRET=

# Optional Flutterwave checkout
FLW_SECRET_KEY=
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,11 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Audit production dependencies
run: npm audit --omit=dev --audit-level=high

- name: Lint source
run: npm run lint

- name: Build and run tests
run: npm test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ yarn-error.log*

# env files (can opt-in for committing if needed)
.env*
!.env.example

# vercel
.vercel
Expand Down
175 changes: 97 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,108 +1,127 @@
# vinext-starter
# EEG ERP — Exploit Engineering Group

A clean full-stack starter running on
[vinext](https://github.com/cloudflare/vinext), with optional Cloudflare D1 and
Drizzle support.
ERP web et PWA terrain d'Exploit Engineering Group. L'application réunit CRM, devis et factures, projets, interventions, GMAO, inspections réglementaires, achats, documents, QHSE, notifications et administration multi-entreprises dans un même parcours opérationnel.

## Prerequisites
Production : [exploit-engineering.nordikperform.chatgpt.site](https://exploit-engineering.nordikperform.chatgpt.site)

Dépôt : [synaval/EEG_ERP](https://github.com/synaval/EEG_ERP)

## Architecture

- Next.js 16, React 19 et TypeScript, compilés par Vinext/Vite pour Cloudflare Workers.
- Cloudflare D1 avec Drizzle ORM pour les données métier et les migrations.
- Cloudflare R2 pour les fichiers, preuves, rapports et logos.
- Authentification Sign in with ChatGPT injectée par Sites.
- Contrôle d'accès multi-tenant par entreprise, rôle et permission.
- PWA installable avec cache hors ligne, file locale d'opérations et synchronisation différée.
- GitHub Actions comme contrôle obligatoire avant intégration sur `main`.

Les routes privées sont sous `app/api/erp`. Les routes `app/api/public` et les liens par jeton sont volontairement publics et limités à la ressource désignée. L'ingestion IoT utilise un jeton d'appareil haché. Les secrets fournisseur ne doivent jamais être placés dans le dépôt.

## Prérequis

- Node.js `>=22.13.0`
- Linux with `flock`, `curl`, and GNU `timeout`
- npm avec le `package-lock.json` du dépôt
- Sous Windows : Git Bash ou WSL pour les scripts npm basés sur Bash
- Sous Linux/CI : `bash`, `curl`, `flock` et GNU `timeout`

## Sites Lifecycle
## Installation et développement

The Sites lifecycle CLI runs the locked dependency install before returning this checkout. Edit the source under `app/`, then checkpoint when a coherent milestone is ready to inspect or share. The remote Sites builder runs `npm run build` against the pushed commit. Do not repeat install or build as a normal pre-checkpoint step.
```bash
npm ci
npm run dev
```

This starter does not use `wrangler.jsonc`.
L'interface est alors servie par Vite/Vinext. Les ressources locales D1 et R2 sont simulées conformément à `.openai/hosting.json` et `vite.config.ts`.

`install:ci` is intentionally a single, non-retrying `npm ci`. It refuses a concurrent install for the same project, consumes a matching image-seeded npm cache with `--prefer-offline` while retaining registry fallback for a missing cache object, otherwise downloads and verifies the complete vinext tarball recorded in `package-lock.json`, limits npm to one socket, and terminates a stalled install. `build` applies a short timeout and then validates the Sites artifact. These helpers target Linux and use GNU `timeout`; they are not native macOS scripts.
Commandes de contrôle :

Scripts that need writable project-scoped home, npm, XDG, and temporary paths use `scripts/sites-env.sh`. The `dev` and `start` scripts honor the caller's runtime environment and keep Wrangler logs inside the checkout. The generated `.sites-runtime/` directory is disposable and ignored by Git.
```bash
npm test
npm run lint
npm run audit:production
npm run validate:artifact
npm run db:generate
```

## Included Shape
`npm test` effectue d'abord une construction vérifiée puis exécute les tests Node : rendu PWA, sécurité des routes, codes agents, service worker et cohérence des migrations. La CI exécute cette commande sur chaque pull request et chaque push vers `main`.

- edit site code under `app/`
- `app/chatgpt-auth.ts` provides optional dispatch-owned ChatGPT sign-in helpers
- `.openai/hosting.json` declares optional Sites D1 and R2 bindings
- `vite.config.ts` simulates declared bindings for local development
- `db/index.ts` reads the D1 binding from the Cloudflare Worker environment
- `db/schema.ts` starts intentionally empty
- `examples/d1/` contains an optional D1 example surface
- `drizzle.config.ts` supports local migration generation when needed
## Configuration d'exécution

## Workspace Auth Headers
Copier `.env.example` uniquement pour un environnement local. Dans Sites, déclarer les variables dans la configuration sécurisée du projet, jamais dans Git.

OpenAI workspace sites can read the current user's email from
`oai-authenticated-user-email`.
| Variable | Obligatoire | Usage |
|---|---:|---|
| `OPENAI_API_KEY` | Non | Assistant IA ERP |
| `OPENAI_MODEL` | Non | Modèle IA, valeur par défaut documentée dans `.env.example` |
| `WAVE_API_KEY` | Non | Paiement Wave |
| `WAVE_SIGNING_SECRET` | Non | Vérification des callbacks Wave |
| `FLW_SECRET_KEY` | Non | Paiement Flutterwave |

SIWC-authenticated workspace sites may also receive
`oai-authenticated-user-full-name` when the user's SIWC profile has a non-empty
`name` claim. The full-name value is percent-encoded UTF-8 and is accompanied by
`oai-authenticated-user-full-name-encoding: percent-encoded-utf-8`.
Sans ces secrets, le cœur ERP reste opérationnel. Les routes IA ou paiement concernées répondent explicitement `503 setupRequired` au lieu de simuler un succès.

Treat the full name as optional and fall back to email when it is absent:
## Base de données et migrations

```tsx
import { headers } from "next/headers";
Le schéma applicatif est dans `db/schema.ts`. Les migrations ordonnées sont dans `drizzle/` et leur journal dans `drizzle/meta/_journal.json`.

export default async function Home() {
const requestHeaders = await headers();
const email = requestHeaders.get("oai-authenticated-user-email");
const encodedFullName = requestHeaders.get("oai-authenticated-user-full-name");
const fullName =
encodedFullName &&
requestHeaders.get("oai-authenticated-user-full-name-encoding") ===
"percent-encoded-utf-8"
? decodeURIComponent(encodedFullName)
: null;
Règles :

const displayName = fullName ?? email;
// ...
}
```
1. Modifier `db/schema.ts`.
2. Générer une migration avec `npm run db:generate`.
3. Relire le SQL ; ne jamais modifier une migration déjà appliquée.
4. Exécuter `npm test`, qui vérifie que chaque entrée du journal possède exactement un fichier SQL.
5. Déployer la nouvelle version par Sites afin que l'historique reste associé au commit publié.

Avant une migration destructive, exporter D1 et sauvegarder les objets R2 concernés. Une restauration doit utiliser l'export D1 et la version R2 pris ensemble au même instant logique.

## Publication et alignement

Les trois références suivantes doivent toujours désigner le même commit :

## Optional Dispatch-Owned ChatGPT Sign-In
1. `main` sur GitHub ;
2. le dépôt local canonique `C:\Users\HP\Downloads\Cabinet Nordik\3. Technologie\Exploit Suit™` ;
3. la source puis la version déployée du projet Sites `appgprj_6a5bb71a4da08191ac1ea8d999e0324d`.

Import the ready-to-use helpers from `app/chatgpt-auth.ts` when the site needs
optional or required ChatGPT sign-in:
Procédure de publication :

- Use `getChatGPTUser()` for optional signed-in UI.
- Use `requireChatGPTUser(returnTo)` for server-rendered pages that should send
anonymous visitors through Sign in with ChatGPT.
- Use `chatGPTSignInPath(returnTo)` and `chatGPTSignOutPath(returnTo)` for
browser links or actions.
- Pass a same-origin relative `returnTo` path for the destination after sign-in
or sign-out. The helper validates and safely encodes it.
- Mark protected pages with `export const dynamic = "force-dynamic"` because
they depend on per-request identity headers.
1. Créer une branche, exécuter `npm test`, pousser et ouvrir une pull request.
2. Attendre la réussite de GitHub Actions puis fusionner sans force-push.
3. Mettre le dépôt canonique à jour avec `git pull --ff-only origin main`.
4. Pousser exactement le SHA de `main` vers la source Sites.
5. Construire et enregistrer une version Sites, la déployer, puis attendre l'état `success`.
6. Comparer les trois SHA et contrôler les journaux Worker.

Dispatch owns `/signin-with-chatgpt`, `/signout-with-chatgpt`, `/callback`, the
OAuth cookies, and identity header injection. Do not implement app routes for
those reserved paths. Routes that do not import and call the helper remain
anonymous-compatible.
## Retour arrière et reprise

SIWC establishes identity only; it does not prove workspace membership. Use the
Sites hosting platform's access policy controls for workspace-wide restrictions,
or enforce explicit server-side membership or allowlist checks.
En cas d'incident applicatif :

Use SIWC for account pages, user-specific dashboards, saved records, and write
actions tied to the current ChatGPT user. Leave public content anonymous.
1. Suspendre les nouvelles écritures si l'intégrité des données est en doute.
2. Identifier le dernier SHA GitHub et la dernière version Sites sains.
3. Redéployer cette version Sites ; ne pas réécrire l'historique Git.
4. Si une migration est en cause, restaurer l'export D1/R2 correspondant après validation métier.
5. Créer un correctif sur une branche, repasser toute la CI et publier selon la procédure d'alignement.
6. Vérifier `/`, `/erp`, les journaux Worker, l'ouverture des devis/factures et un parcours terrain hors ligne avant réouverture.

## Diagnostic Commands
## Sécurité et accès

- `npm run install:ci`: perform the one bounded lockfile install
- `npm run dev`: start the Vite/Vinext development server
- `npm run build`: build and validate the deployable Sites artifact
- `npm run start`: start the built Vinext application
- `npm test`: build, validate, and verify the rendered development-preview metadata
- `npm run validate:artifact`: recheck an existing artifact's manifest and ESM `default.fetch` export
- `npm run db:generate`: generate Drizzle migrations after schema changes
- Deux comptes explicitement configurés dans `app/crm/auth.ts` disposent du rôle super-administrateur.
- Les autres utilisateurs doivent avoir une adhésion active à une entreprise et les permissions nécessaires.
- Les invitations sont liées à l'adresse ChatGPT authentifiée, expirent et sont journalisées.
- Les codes agents sont normalisés puis hachés avec PBKDF2-SHA-256 et un sel aléatoire.
- Les routes de données vérifient le périmètre entreprise avant lecture ou écriture.
- La politique d'accès Sites constitue une barrière supplémentaire et doit être élargie volontairement avant un pilote multi-utilisateur.

Use build and validation commands for targeted diagnosis after a remote failure, not as part of the normal checkpoint path.
## Validation avant mise en service

The timeout defaults can be overridden for a controlled canary with `SITES_INSTALL_TIMEOUT`, `SITES_INSTALL_KILL_AFTER`, `SITES_BUILD_TIMEOUT`, and `SITES_BUILD_KILL_AFTER`. A timeout fails the command; the helpers never retry an unchanged install or build.
- CI verte sur le SHA publié.
- GitHub, dépôt canonique et Sites sur le même SHA.
- Aucun secret dans Git ni dans les journaux.
- Variables IA/paiement configurées uniquement si les fonctions sont activées.
- Politique d'accès Sites et utilisateurs pilotes validés.
- Test métier manuel : client → devis → acceptation → facture → paiement.
- Test terrain manuel : installation PWA → intervention → photo/signature → perte réseau → resynchronisation.
- Sauvegarde D1/R2 et procédure de restauration testées sur un environnement non productif.

## Learn More
## Limites opérationnelles explicites

- [vinext Documentation](https://github.com/cloudflare/vinext)
- [Drizzle D1 Guide](https://orm.drizzle.team/docs/get-started/d1-new)
L'activation réelle des paiements, de l'IA et d'un pilote multi-utilisateur dépend respectivement de secrets marchands, d'une clé OpenAI et d'une décision d'accès externe. Ces éléments ne peuvent pas être fabriqués par le code et doivent être fournis ou approuvés par le propriétaire du projet.
34 changes: 34 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Politique de sécurité

## Signalement

Ne publiez jamais une vulnérabilité contenant des données clients dans une issue publique. Transmettez le scénario, la route concernée, l'impact et une preuve minimale au propriétaire du dépôt afin qu'un correctif privé puisse être préparé et testé.

## Contrôles obligatoires

Chaque pull request exécute :

- installation déterministe avec `npm ci` ;
- audit des dépendances réellement déployées avec `npm audit --omit=dev --audit-level=high` ;
- lint complet ;
- build Vinext et validation de l'artefact Worker ;
- tests d'authentification, PWA, migrations et codes agents.

Les secrets OpenAI, Wave et Flutterwave sont configurés dans Sites et ne doivent jamais être committés. Les fichiers `.env` restent ignorés ; seul `.env.example`, sans valeur secrète, est versionné.

## Risques de chaîne de build acceptés temporairement

L'audit complet peut encore signaler des dépendances de développement que l'audit de production exclut :

- `image-size`, imposé par Vinext, est utilisé pendant la construction et n'est pas appelé par le code métier sur les fichiers téléversés ; aucune version corrigée n'est actuellement publiée dans la branche Vinext utilisée par Sites.
- une ancienne version d'`esbuild`, transitive de `drizzle-kit`, est limitée à la génération locale de migrations. Le serveur de développement ne doit jamais être exposé sur un réseau non fiable.

Ces exceptions ne couvrent pas le runtime de production, pour lequel l'audit doit rester à zéro vulnérabilité élevée. Elles doivent être supprimées dès qu'une version compatible de Vinext ou Drizzle corrige les dépendances transitives. Tout changement de version doit repasser l'intégralité de la CI et un déploiement de validation.

## Réponse à incident

1. Révoquer immédiatement tout secret potentiellement exposé.
2. Restreindre l'accès Sites et suspendre les écritures si l'intégrité des données est incertaine.
3. Conserver les journaux Worker et identifier le SHA déployé.
4. Redéployer la dernière version saine ou restaurer l'instantané D1/R2 cohérent.
5. Publier le correctif via pull request, CI et procédure d'alignement documentée dans le README.
6 changes: 1 addition & 5 deletions app/crm/EegErpApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -908,10 +908,6 @@ export default function EegErpApp({ userName }: { userName: string }) {
const parent = moduleGroups.find((group) => (group.ids as readonly string[]).includes(target));
if (parent) setOpenGroups((current) => current.includes(parent.label) ? current : [...current, parent.label]);
}, []);
useEffect(() => {
const parent = moduleGroups.find((group) => (group.ids as readonly string[]).includes(module));
if (parent && !openGroups.includes(parent.label)) setOpenGroups((current) => [...current, parent.label]);
}, [module, openGroups]);
const toggleGroup = (label: string) => setOpenGroups((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]);
const navigateHse = (targetView: string) => {
setModule("hse");
Expand Down Expand Up @@ -972,7 +968,7 @@ export default function EegErpApp({ userName }: { userName: string }) {
{moduleGroups.map((group) => {
const entries = visibleModules.filter(([id]) => id !== "home" && (group.ids as readonly string[]).includes(id));
if (!entries.length) return null;
const expanded = openGroups.includes(group.label);
const expanded = openGroups.includes(group.label) || (group.ids as readonly string[]).includes(module);
const hasActive = entries.some(([id]) => id === module);
return <div className={`erp-nav-group ${expanded ? "open" : ""} ${hasActive ? "has-active" : ""}`} key={group.label}>
<button type="button" className="erp-nav-group-trigger" onClick={() => toggleGroup(group.label)} aria-expanded={expanded}>
Expand Down
4 changes: 4 additions & 0 deletions app/crm/IntegratedManagementSuite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@ const toolValues=(row?:Row)=>{try{const parsed=JSON.parse(String(row?.requiremen
export default function IntegratedManagementSuite({domain,view,companyId}:{domain:"hse"|"hr";view:string;companyId:number|null}){
const [data,setData]=useState<Data>(empty),[loading,setLoading]=useState(true),[message,setMessage]=useState(""),[form,setForm]=useState<"record"|"employee"|null>(null),[editing,setEditing]=useState<Row|null>(null);
const load=useCallback(async()=>{if(!companyId){setLoading(false);return}setLoading(true);try{const r=await fetch(`/api/erp/integrated-suite?tenantId=${companyId}&domain=${domain}`,{cache:"no-store"});const b=await r.json();if(!r.ok)throw new Error(b.error);setData(b)}catch(e){setMessage(e instanceof Error?e.message:"Chargement impossible")}finally{setLoading(false)}},[companyId,domain]);
// This effect intentionally synchronizes tenant selection with the remote ERP store.
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(()=>{void load()},[load]);
const post=async(payload:Record<string,unknown>)=>{const r=await fetch(`/api/erp/integrated-suite?tenantId=${companyId}&domain=${domain}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});const b=await r.json();if(!r.ok){setMessage(b.error||"Opération impossible");return false}await load();return true};
const type=viewType[view];const records=(domain==="hse"?data.hse:data.hr).filter(r=>!type||r.recordType===type);const openFlows=data.workflows.filter(w=>w.status==="open");const tool=domain==="hse"&&type?(hseToolConfigs[type]||fallbackTool(type)):undefined;
// The expiry horizon is deliberately evaluated when fresh tenant data is rendered.
// eslint-disable-next-line react-hooks/purity
const stats=useMemo(()=>domain==="hse"?[["Outils actifs",new Set(data.hse.map(x=>x.recordType)).size],["Registres",data.hse.length],["Critiques",data.hse.filter(x=>x.severity==="critical").length],["Workflows ouverts",openFlows.filter(x=>x.targetType==="hse_record").length]]:[["Collaborateurs",data.people.length],["Dossiers RH",data.hr.length],["Échéances",data.hr.filter(x=>x.expiryDate&&new Date(String(x.expiryDate))<new Date(Date.now()+90*86400000)).length],["Validations",openFlows.filter(x=>x.targetType==="hr_record").length]],[data,domain,openFlows]);
if(!companyId)return <div className="suite-empty">Sélectionnez une entreprise : toutes les données HSE et RH sont isolées par tenant.</div>;
if(loading)return <div className="suite-empty">Chargement du système intégré…</div>;
Expand Down
17 changes: 1 addition & 16 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "Exploit Engineering Group",
description: "Ingénierie, construction, maintenance et plateforme opérationnelle EEG.",
Expand Down Expand Up @@ -45,11 +34,7 @@ export default function RootLayout({
}>) {
return (
<html lang="fr">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
<body>{children}</body>
</html>
);
}
Loading
Loading