@mostajs/pdf-sign-ui 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/CHANGELOG.md +16 -0
- package/README.md +92 -0
- package/llms.txt +43 -0
- package/package.json +28 -0
- package/src/headless.js +195 -0
- package/src/index.js +10 -0
- package/src/presets.js +35 -0
- package/src/react/EngineBadge.jsx +23 -0
- package/src/react/SignForm.jsx +113 -0
- package/src/react/VerifyPanel.jsx +60 -0
- package/src/react/index.js +5 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog — @mostajs/pdf-sign-ui
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
|
|
5
|
+
Le format suit [Keep a Changelog](https://keepachangelog.com/fr/) ; versionnage [SemVer](https://semver.org/lang/fr/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — 2026-07-01
|
|
8
|
+
|
|
9
|
+
### Ajouté
|
|
10
|
+
|
|
11
|
+
- Logique **headless** de la couche de présentation de `@mostajs/pdf-sign` : `validateSignForm`, `buildSignRequest`, `buildEngineViewModel`, `buildVerifyViewModel`, `createPdfSignUiController`.
|
|
12
|
+
- Préréglages de documents officiels (générique, cadré SIH) : `DOCUMENT_PRESETS`, `listPresets`, `getPreset` — clinique / administratif / financier CNAS-CASNOS.
|
|
13
|
+
- Contrôleur orchestrant un `service` injecté (moteur in-process ou handlers).
|
|
14
|
+
- Composants **React** (`@mostajs/pdf-sign-ui/react`, peer `react` optionnelle) : `<EngineBadge>`, `<SignForm>` (préréglage + métadonnées + permissions + cachet image), `<VerifyPanel>` (tableau des signatures). En `React.createElement` (sans JSX à transpiler).
|
|
15
|
+
- Passthrough `field.stampImage` dans `buildSignRequest` (cachet visible image).
|
|
16
|
+
- Tests `@mostajs/mjs-unit` (headless + rendu SSR React) et exemple `examples/verify-viewmodel.mjs`.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @mostajs/pdf-sign-ui
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
**Licence** : AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
Couche de **présentation** de [`@mostajs/pdf-sign`](../mosta-pdf-sign). Logique **headless** (framework-agnostique, zéro dépendance, testable sans navigateur) : validation du formulaire de signature, préréglages de documents officiels, view-models d'état moteur et de vérification, contrôleur orchestrant le moteur.
|
|
7
|
+
|
|
8
|
+
Le module ne signe pas lui-même : il pilote `@mostajs/pdf-sign` via un **`service` injecté** (in-process ou handlers HTTP).
|
|
9
|
+
|
|
10
|
+
## Cadrage SIH (générique, surchargeable)
|
|
11
|
+
|
|
12
|
+
Des préréglages couvrent les documents à valeur probante d'un SIH — tout en restant génériques :
|
|
13
|
+
|
|
14
|
+
- **clinique** : `crh`, `cro`, `imagerie`, `biologie`
|
|
15
|
+
- **administratif** : `ordonnance`, `certificat_sejour`, `dossier_sortie`
|
|
16
|
+
- **financier / CNAS-CASNOS** : `facture`, `bordereau`, `prise_en_charge`
|
|
17
|
+
|
|
18
|
+
Chaque préréglage propose un motif (`reason`) et une recommandation `restrict` (permissions AES-256, moteur pyhanko). L'appelant peut tout surcharger.
|
|
19
|
+
|
|
20
|
+
## API
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import {
|
|
24
|
+
validateSignForm, buildSignRequest, buildVerifyViewModel,
|
|
25
|
+
buildEngineViewModel, createPdfSignUiController, listPresets,
|
|
26
|
+
} from "@mostajs/pdf-sign-ui";
|
|
27
|
+
import * as engine from "@mostajs/pdf-sign"; // service in-process
|
|
28
|
+
|
|
29
|
+
const ctl = createPdfSignUiController({ service: engine, t: monI18n });
|
|
30
|
+
|
|
31
|
+
// Bandeau moteur
|
|
32
|
+
console.log(ctl.engines.label, ctl.engines.canRestrict);
|
|
33
|
+
|
|
34
|
+
// Soumission d'un formulaire (validation intégrée)
|
|
35
|
+
const res = await ctl.submit({
|
|
36
|
+
input: "crh.pdf", output: "crh.signe.pdf",
|
|
37
|
+
p12: "praticien.p12", passphrase: process.env.P12_PASS,
|
|
38
|
+
preset: "crh", // motif + permissions par défaut
|
|
39
|
+
name: "Dr H. Madani", location: "Alger, DZ",
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) console.error(res.errors); else console.log(res.result);
|
|
42
|
+
|
|
43
|
+
// Vérification (view-model prêt pour un tableau)
|
|
44
|
+
console.log(ctl.verify("crh.signe.pdf", { trustPem: "ac-racine.pem" }));
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Fonctions pures (sans contrôleur)
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
const { valid, errors } = validateSignForm(formData, { engines: { node: true, pyhanko: true } });
|
|
51
|
+
const signOptions = buildSignRequest(formData); // → SignOptions de @mostajs/pdf-sign
|
|
52
|
+
const vm = buildVerifyViewModel(verifyResult); // → { signed, status, rows[] }
|
|
53
|
+
const badge = buildEngineViewModel(detectEnginesResult);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Contrat du `service`
|
|
57
|
+
|
|
58
|
+
`createPdfSignUiController` exige un objet exposant :
|
|
59
|
+
|
|
60
|
+
| Méthode | Signature | Origine |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `signPdf(opts)` | `Promise<SignResult>` | `@mostajs/pdf-sign` |
|
|
63
|
+
| `verifyPdf(opts)` | `VerifyResult` | `@mostajs/pdf-sign` |
|
|
64
|
+
| `detectEngines()` | `EngineAvailability` | `@mostajs/pdf-sign` |
|
|
65
|
+
|
|
66
|
+
En architecture web, ces méthodes peuvent être des appels à des handlers serveur (le moteur — Node + Python — reste côté serveur).
|
|
67
|
+
|
|
68
|
+
## Composants React (`@mostajs/pdf-sign-ui/react`)
|
|
69
|
+
|
|
70
|
+
Peer optionnelle `react >=18`. Composants en `React.createElement` (aucune syntaxe JSX ; un bundler résout les `.jsx`), pilotés par la logique headless.
|
|
71
|
+
|
|
72
|
+
| Composant | Rôle |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `<EngineBadge availability|engines t />` | bandeau d'état du moteur (PAdES / permissions disponibles) |
|
|
75
|
+
| `<SignForm service t presets initialData onSigned />` | formulaire de signature : préréglage de document, certificat, métadonnées, moteur, permissions, cachet image ; validation intégrée |
|
|
76
|
+
| `<VerifyPanel service t input trustPem auto />` | vérification d'un PDF + tableau des signatures (intègre / valide / confiance / signataire) |
|
|
77
|
+
|
|
78
|
+
```jsx
|
|
79
|
+
import { SignForm, VerifyPanel, EngineBadge } from "@mostajs/pdf-sign-ui/react";
|
|
80
|
+
|
|
81
|
+
<SignForm service={engine} t={t} onSigned={(r) => console.log(r)} />
|
|
82
|
+
<VerifyPanel service={engine} t={t} trustPem="ac-racine.pem" />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Le `service` est le même contrat que le contrôleur (`signPdf`/`verifyPdf`/`detectEngines`). En web, ce sont typiquement des appels aux handlers serveur (le moteur Node + Python reste côté serveur).
|
|
86
|
+
|
|
87
|
+
## Tests
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm test # logique headless (@mostajs/mjs-unit)
|
|
91
|
+
npm run test:react # rendu SSR des composants (react + react-dom requis)
|
|
92
|
+
```
|
package/llms.txt
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @mostajs/pdf-sign-ui — fiche LLM
|
|
2
|
+
> Couche de PRÉSENTATION de @mostajs/pdf-sign : logique HEADLESS (validation du formulaire de signature, préréglages de documents officiels/SIH, view-models moteur & vérification, contrôleur orchestrant un `service` injecté). Framework-agnostique, zéro dépendance, testable sans navigateur. IMPLÉMENTÉ 0.1.0.
|
|
3
|
+
|
|
4
|
+
- Version: 0.1.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
- Chemin: mostajs/mosta-pdf-sign-stack/mosta-pdf-sign-ui · Moteur: ../mosta-pdf-sign
|
|
6
|
+
|
|
7
|
+
## RÔLE
|
|
8
|
+
Présentation UNIQUEMENT. Ne signe pas : orchestre @mostajs/pdf-sign via un `service` injecté
|
|
9
|
+
(in-process ou handlers). Générique ; le cadrage SIH est apporté par des préréglages surchargables.
|
|
10
|
+
|
|
11
|
+
## EXPORTS ("@mostajs/pdf-sign-ui")
|
|
12
|
+
- validateSignForm(data,{engines,t}) -> {valid,errors} — input/p12/passphrase requis ; permissions ⇒ pyhanko
|
|
13
|
+
- buildSignRequest(data) -> SignOptions — applique préréglage (reason+restrict), surchargé par la saisie, élague les vides
|
|
14
|
+
- buildEngineViewModel(availability,{t}) -> {node,pyhanko,canRestrict,canPades,label,hint}
|
|
15
|
+
- buildVerifyViewModel(verifyResult,{t}) -> {signed,engine,status,rows[{field,intact,valid,trusted,signer,status}]}
|
|
16
|
+
- createPdfSignUiController({service,t}) -> { availability, engines, submit(form), verify(input,opts) }
|
|
17
|
+
- DOCUMENT_PRESETS / listPresets() / getPreset(key)
|
|
18
|
+
|
|
19
|
+
## PRÉRÉGLAGES (générique, cadré SIH — surchargables)
|
|
20
|
+
clinique: crh, cro, imagerie, biologie · administratif: ordonnance, certificat_sejour, dossier_sortie
|
|
21
|
+
financier/CNAS-CASNOS: facture, bordereau, prise_en_charge. Chaque preset = { reason, restrict }.
|
|
22
|
+
restrict:true ⇒ moteur pyhanko requis (permissions AES-256).
|
|
23
|
+
|
|
24
|
+
## CONTRÔLEUR
|
|
25
|
+
createPdfSignUiController EXIGE `service` = { signPdf, verifyPdf, detectEngines } (sinon throw).
|
|
26
|
+
submit(form) : valide → buildSignRequest → service.signPdf ; renvoie {ok,errors?|result?}.
|
|
27
|
+
verify(input) : service.verifyPdf → buildVerifyViewModel.
|
|
28
|
+
|
|
29
|
+
## DÉPEND DE
|
|
30
|
+
- @mostajs/pdf-sign (peer OPTIONNELLE) — pour les types et le service in-process. La logique headless
|
|
31
|
+
tourne sans lui (service simulé / handlers).
|
|
32
|
+
|
|
33
|
+
## EXPORTS REACT ("@mostajs/pdf-sign-ui/react" — peer react optionnelle)
|
|
34
|
+
- <EngineBadge availability|engines t /> — bandeau capacités moteur
|
|
35
|
+
- <SignForm service t presets initialData onSigned /> — formulaire (preset+meta+permissions+cachet image), validation intégrée
|
|
36
|
+
- <VerifyPanel service t input trustPem auto /> — vérif + tableau des signatures
|
|
37
|
+
Composants en React.createElement (pas de JSX à transpiler ; bundler pour les .jsx). SSR-testés (5 tests).
|
|
38
|
+
|
|
39
|
+
## PIÈGES
|
|
40
|
+
- Le cœur Node ne gère pas les permissions : validateSignForm rejette permissions+engine:node.
|
|
41
|
+
- box = [x1,y1,x2,y2] nombres finis ; sinon rejet. t (i18n) optionnel → identité.
|
|
42
|
+
- Composants React sans build JSX (React.createElement) : Node natif ne charge pas .jsx → test via loader (test:react) ; en prod un bundler résout .jsx.
|
|
43
|
+
- field.stampImage propagé par buildSignRequest (cachet image, moteur pyhanko).
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/pdf-sign-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Couche de présentation de @mostajs/pdf-sign — logique headless (validation du formulaire de signature, préréglages de documents officiels/SIH, view-models de vérification, contrôleur). Framework-agnostique, zéro dépendance.",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js",
|
|
11
|
+
"./react": "./src/react/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": ["src", "README.md", "llms.txt", "CHANGELOG.md"],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node test-scripts/headless.test.mjs",
|
|
16
|
+
"test:react": "node --experimental-loader ./test-scripts/jsx-loader.mjs test-scripts/react.test.mjs",
|
|
17
|
+
"example": "node examples/verify-viewmodel.mjs"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@mostajs/pdf-sign": "^0.1.0",
|
|
21
|
+
"react": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"peerDependenciesMeta": {
|
|
24
|
+
"@mostajs/pdf-sign": { "optional": true },
|
|
25
|
+
"react": { "optional": true }
|
|
26
|
+
},
|
|
27
|
+
"keywords": ["mostajs", "pdf", "signature", "ui", "headless", "sih", "pades"]
|
|
28
|
+
}
|
package/src/headless.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// @mostajs/pdf-sign-ui — logique headless (framework-agnostique, zéro dépendance).
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
//
|
|
4
|
+
// Couche de PRÉSENTATION de @mostajs/pdf-sign : valide un formulaire de signature,
|
|
5
|
+
// construit les SignOptions, expose des view-models de vérification, orchestre via un
|
|
6
|
+
// `service` injecté (in-process @mostajs/pdf-sign ou handlers HTTP). Ne signe pas elle-même.
|
|
7
|
+
|
|
8
|
+
import { getPreset } from "./presets.js";
|
|
9
|
+
|
|
10
|
+
/** Identité par défaut si aucune fonction de traduction n'est fournie. */
|
|
11
|
+
const IDENTITY = (s) => s;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Valide le formulaire de signature.
|
|
15
|
+
* @param {object} data { input, p12, passphrase, passphraseFile, engine, box, page, permissions, preset }
|
|
16
|
+
* @param {object} [opts] { engines?: {node,pyhanko}, t?: (k)=>string }
|
|
17
|
+
* @returns {{ valid: boolean, errors: Record<string,string> }}
|
|
18
|
+
*/
|
|
19
|
+
export function validateSignForm(data = {}, opts = {}) {
|
|
20
|
+
const t = opts.t || IDENTITY;
|
|
21
|
+
const engines = opts.engines || { node: true, pyhanko: false };
|
|
22
|
+
const errors = {};
|
|
23
|
+
|
|
24
|
+
if (!data.input) errors.input = t("Le PDF à signer est requis.");
|
|
25
|
+
if (!data.p12) errors.p12 = t("Le certificat PKCS#12 est requis.");
|
|
26
|
+
if (!data.passphrase && !data.passphraseFile) {
|
|
27
|
+
errors.passphrase = t("La phrase secrète (ou son fichier) est requise.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const engine = data.engine || "auto";
|
|
31
|
+
if (!["auto", "node", "pyhanko"].includes(engine)) {
|
|
32
|
+
errors.engine = t("Moteur inconnu (auto | node | pyhanko).");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const wantsPerms = data.permissions === true || (data.permissions && typeof data.permissions === "object");
|
|
36
|
+
if (wantsPerms) {
|
|
37
|
+
if (engine === "node") errors.permissions = t("Le cœur Node ne gère pas les permissions — choisir pyhanko.");
|
|
38
|
+
else if (!engines.pyhanko) errors.permissions = t("Permissions restrictives indisponibles : moteur pyhanko requis.");
|
|
39
|
+
}
|
|
40
|
+
if (engine === "pyhanko" && !engines.pyhanko) {
|
|
41
|
+
errors.engine = t("Moteur pyhanko indisponible sur ce serveur.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (data.box != null && !isBox(data.box)) {
|
|
45
|
+
errors.box = t("Boîte du cachet invalide : attendu [x1, y1, x2, y2] en points.");
|
|
46
|
+
}
|
|
47
|
+
if (data.page != null && (!Number.isInteger(Number(data.page)) || Number(data.page) < 0)) {
|
|
48
|
+
errors.page = t("Numéro de page invalide (entier ≥ 0).");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { valid: Object.keys(errors).length === 0, errors };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isBox(b) {
|
|
55
|
+
return Array.isArray(b) && b.length === 4 && b.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Construit les SignOptions (@mostajs/pdf-sign) à partir des données du formulaire.
|
|
60
|
+
* Applique le préréglage de document choisi (motif + permissions), surchargé par les champs saisis.
|
|
61
|
+
* @param {object} data
|
|
62
|
+
* @returns {import("@mostajs/pdf-sign").SignOptions}
|
|
63
|
+
*/
|
|
64
|
+
export function buildSignRequest(data = {}) {
|
|
65
|
+
const preset = data.preset ? getPreset(data.preset) : undefined;
|
|
66
|
+
const reason = data.reason || preset?.reason;
|
|
67
|
+
|
|
68
|
+
// permissions : objet explicite > drapeau restrict > préréglage
|
|
69
|
+
let permissions;
|
|
70
|
+
if (data.permissions && typeof data.permissions === "object") permissions = data.permissions;
|
|
71
|
+
else if (data.permissions === true || data.restrict === true) permissions = {};
|
|
72
|
+
else if (data.permissions === false || data.restrict === false) permissions = undefined;
|
|
73
|
+
else if (preset?.restrict) permissions = {};
|
|
74
|
+
|
|
75
|
+
/** @type {any} */
|
|
76
|
+
const req = {
|
|
77
|
+
input: data.input,
|
|
78
|
+
output: data.output,
|
|
79
|
+
p12: data.p12,
|
|
80
|
+
passphrase: data.passphrase,
|
|
81
|
+
passphraseFile: data.passphraseFile,
|
|
82
|
+
engine: data.engine || "auto",
|
|
83
|
+
meta: prune({
|
|
84
|
+
reason,
|
|
85
|
+
name: data.name,
|
|
86
|
+
location: data.location,
|
|
87
|
+
contact: data.contact,
|
|
88
|
+
}),
|
|
89
|
+
field: prune({ page: numOrUndef(data.page), box: isBox(data.box) ? data.box : undefined, stampImage: data.stampImage }),
|
|
90
|
+
permissions,
|
|
91
|
+
};
|
|
92
|
+
return prune(req);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function prune(obj) {
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const [k, v] of Object.entries(obj)) if (v !== undefined) out[k] = v;
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
function numOrUndef(v) {
|
|
101
|
+
return v == null || v === "" ? undefined : Number(v);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* View-model de l'état des moteurs (pour un bandeau/badge d'UI).
|
|
106
|
+
* @param {{ node: boolean, pyhanko: boolean, python?: string }} availability
|
|
107
|
+
* @param {object} [opts] { t }
|
|
108
|
+
*/
|
|
109
|
+
export function buildEngineViewModel(availability = { node: true, pyhanko: false }, opts = {}) {
|
|
110
|
+
const t = opts.t || IDENTITY;
|
|
111
|
+
const pyhanko = !!availability.pyhanko;
|
|
112
|
+
return {
|
|
113
|
+
node: !!availability.node,
|
|
114
|
+
pyhanko,
|
|
115
|
+
canRestrict: pyhanko,
|
|
116
|
+
canPades: pyhanko,
|
|
117
|
+
label: pyhanko ? t("PAdES + permissions (pyhanko)") : t("PKCS#7 (Node)"),
|
|
118
|
+
hint: pyhanko
|
|
119
|
+
? t("Signature avancée et permissions restrictives disponibles.")
|
|
120
|
+
: t("Moteur pyhanko absent : signature simple uniquement (lancer setup-venv.sh pour PAdES)."),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* View-model d'un résultat de vérification (lignes prêtes à afficher).
|
|
126
|
+
* @param {import("@mostajs/pdf-sign").VerifyResult} result
|
|
127
|
+
* @param {object} [opts] { t }
|
|
128
|
+
*/
|
|
129
|
+
export function buildVerifyViewModel(result, opts = {}) {
|
|
130
|
+
const t = opts.t || IDENTITY;
|
|
131
|
+
if (!result || !result.signed) {
|
|
132
|
+
return { signed: false, engine: result?.engine, status: t("Non signé"), rows: [] };
|
|
133
|
+
}
|
|
134
|
+
const rows = (result.signatures || []).map((s, i) => ({
|
|
135
|
+
field: s.field || `Signature${i + 1}`,
|
|
136
|
+
intact: s.intact,
|
|
137
|
+
valid: s.valid,
|
|
138
|
+
trusted: s.trusted,
|
|
139
|
+
signer: s.signer,
|
|
140
|
+
status: statusLabel(s, t),
|
|
141
|
+
}));
|
|
142
|
+
const allValid = rows.length > 0 && rows.every((r) => r.valid !== false && r.intact !== false);
|
|
143
|
+
return {
|
|
144
|
+
signed: true,
|
|
145
|
+
engine: result.engine,
|
|
146
|
+
status: allValid ? t("Signature(s) valide(s)") : t("Signature(s) à vérifier"),
|
|
147
|
+
rows,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function statusLabel(s, t) {
|
|
152
|
+
if (s.valid === false || s.intact === false) return t("Invalide");
|
|
153
|
+
if (s.trusted) return t("Valide et de confiance");
|
|
154
|
+
if (s.valid || s.intact) return t("Valide (confiance non établie)");
|
|
155
|
+
return t("Présente (non validée)");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Contrôleur UI : orchestre validation → signature → vérification via un `service` injecté.
|
|
160
|
+
* `service` doit exposer { signPdf, verifyPdf, detectEngines } (in-process @mostajs/pdf-sign
|
|
161
|
+
* ou handlers). EXIGE `service` (sinon throw), à l'image de createUsersUiController.
|
|
162
|
+
* @param {object} cfg { service, t? }
|
|
163
|
+
*/
|
|
164
|
+
export function createPdfSignUiController(cfg = {}) {
|
|
165
|
+
const { service, t = IDENTITY } = cfg;
|
|
166
|
+
if (!service || typeof service.signPdf !== "function") {
|
|
167
|
+
throw new Error("createPdfSignUiController : `service` avec signPdf/verifyPdf/detectEngines requis.");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const availability = typeof service.detectEngines === "function"
|
|
171
|
+
? service.detectEngines()
|
|
172
|
+
: { node: true, pyhanko: false };
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
availability,
|
|
176
|
+
engines: buildEngineViewModel(availability, { t }),
|
|
177
|
+
|
|
178
|
+
/** Valide puis signe. Renvoie { ok, errors?, result? }. */
|
|
179
|
+
async submit(formData) {
|
|
180
|
+
const { valid, errors } = validateSignForm(formData, { engines: availability, t });
|
|
181
|
+
if (!valid) return { ok: false, errors };
|
|
182
|
+
const result = await service.signPdf(buildSignRequest(formData));
|
|
183
|
+
return { ok: true, result };
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
/** Vérifie un PDF et renvoie un view-model d'affichage. */
|
|
187
|
+
verify(input, verifyOpts = {}) {
|
|
188
|
+
if (typeof service.verifyPdf !== "function") {
|
|
189
|
+
throw new Error("Le service ne fournit pas verifyPdf.");
|
|
190
|
+
}
|
|
191
|
+
const raw = service.verifyPdf({ input, ...verifyOpts });
|
|
192
|
+
return buildVerifyViewModel(raw, { t });
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @mostajs/pdf-sign-ui — point d'entrée (logique headless, framework-agnostique).
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
export {
|
|
4
|
+
validateSignForm,
|
|
5
|
+
buildSignRequest,
|
|
6
|
+
buildEngineViewModel,
|
|
7
|
+
buildVerifyViewModel,
|
|
8
|
+
createPdfSignUiController,
|
|
9
|
+
} from "./headless.js";
|
|
10
|
+
export { DOCUMENT_PRESETS, listPresets, getPreset } from "./presets.js";
|
package/src/presets.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @mostajs/pdf-sign-ui — préréglages de documents officiels (générique, cadré SIH).
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
//
|
|
4
|
+
// Chaque préréglage propose un `reason` (motif du cachet) et `restrict` (permissions
|
|
5
|
+
// AES-256 recommandées). GÉNÉRIQUE : purement indicatif, l'appelant peut tout surcharger.
|
|
6
|
+
// Les préréglages `restrict:true` exigent le moteur pyhanko.
|
|
7
|
+
|
|
8
|
+
/** @typedef {{ key: string, label: string, category: string, reason: string, restrict: boolean }} DocumentPreset */
|
|
9
|
+
|
|
10
|
+
/** @type {Record<string, DocumentPreset>} */
|
|
11
|
+
export const DOCUMENT_PRESETS = {
|
|
12
|
+
// — Clinique —
|
|
13
|
+
crh: { key: "crh", label: "Compte rendu d'hospitalisation (CRH)", category: "clinique", reason: "Compte rendu d'hospitalisation", restrict: true },
|
|
14
|
+
cro: { key: "cro", label: "Compte rendu opératoire (CRO)", category: "clinique", reason: "Compte rendu opératoire", restrict: true },
|
|
15
|
+
imagerie: { key: "imagerie", label: "Compte rendu d'imagerie", category: "clinique", reason: "Compte rendu d'imagerie", restrict: true },
|
|
16
|
+
biologie: { key: "biologie", label: "Compte rendu de biologie", category: "clinique", reason: "Compte rendu de biologie", restrict: true },
|
|
17
|
+
// — Administratif —
|
|
18
|
+
ordonnance: { key: "ordonnance", label: "Ordonnance", category: "administratif", reason: "Ordonnance médicale", restrict: true },
|
|
19
|
+
certificat_sejour: { key: "certificat_sejour", label: "Certificat de séjour", category: "administratif", reason: "Certificat de séjour", restrict: true },
|
|
20
|
+
dossier_sortie: { key: "dossier_sortie", label: "Dossier de sortie", category: "administratif", reason: "Dossier de sortie", restrict: false },
|
|
21
|
+
// — Financier / CNAS-CASNOS —
|
|
22
|
+
facture: { key: "facture", label: "Facture (CNAS/CASNOS)", category: "financier", reason: "Facture — valeur probante", restrict: true },
|
|
23
|
+
bordereau: { key: "bordereau", label: "Bordereau de remboursement", category: "financier", reason: "Bordereau de remboursement", restrict: true },
|
|
24
|
+
prise_en_charge: { key: "prise_en_charge", label: "Prise en charge", category: "financier", reason: "Prise en charge", restrict: true },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Liste ordonnée des préréglages (pour peupler un menu déroulant). */
|
|
28
|
+
export function listPresets() {
|
|
29
|
+
return Object.values(DOCUMENT_PRESETS);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Renvoie le préréglage pour une clé, ou `undefined`. */
|
|
33
|
+
export function getPreset(key) {
|
|
34
|
+
return DOCUMENT_PRESETS[key];
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @mostajs/pdf-sign-ui/react — <EngineBadge> : bandeau d'état des moteurs.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { buildEngineViewModel } from '../headless.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Affiche le moteur actif et ses capacités (PAdES / permissions).
|
|
8
|
+
* @param {{ availability?: {node,pyhanko,python?}, engines?: object, t?: (k)=>string }} props
|
|
9
|
+
* Fournir `availability` (sortie de detectEngines) OU `engines` (déjà construit, ex. controller.engines).
|
|
10
|
+
*/
|
|
11
|
+
export function EngineBadge({ availability, engines, t = (k) => k }) {
|
|
12
|
+
const vm = engines || buildEngineViewModel(availability || { node: true, pyhanko: false }, { t });
|
|
13
|
+
const cls = vm.pyhanko ? 'ok' : 'warn';
|
|
14
|
+
return React.createElement('div', { className: `mosta-pdf-sign-engine ${cls}`, role: 'status' },
|
|
15
|
+
React.createElement('strong', null, vm.label),
|
|
16
|
+
React.createElement('span', { className: 'mosta-pdf-sign-engine-hint' }, vm.hint),
|
|
17
|
+
React.createElement('ul', { className: 'mosta-pdf-sign-engine-caps' },
|
|
18
|
+
React.createElement('li', null, `${t('Signature avancée (PAdES)')} : ${vm.canPades ? '✓' : '—'}`),
|
|
19
|
+
React.createElement('li', null, `${t('Permissions AES-256')} : ${vm.canRestrict ? '✓' : '—'}`),
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
export default EngineBadge;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @mostajs/pdf-sign-ui/react — <SignForm> : formulaire de signature (wrapper headless).
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
//
|
|
4
|
+
// Piloté par createPdfSignUiController : validation intégrée, préréglages de documents,
|
|
5
|
+
// bascule permissions/moteur selon les capacités serveur. Le `service` (in-process ou
|
|
6
|
+
// handlers) porte signPdf/verifyPdf/detectEngines.
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { createPdfSignUiController, validateSignForm } from '../headless.js';
|
|
9
|
+
import { listPresets } from '../presets.js';
|
|
10
|
+
import { EngineBadge } from './EngineBadge.jsx';
|
|
11
|
+
|
|
12
|
+
const h = React.createElement;
|
|
13
|
+
|
|
14
|
+
export function SignForm({ service, t = (k) => k, presets = listPresets(), initialData = {}, onSigned }) {
|
|
15
|
+
const controller = React.useMemo(() => createPdfSignUiController({ service, t }), [service]);
|
|
16
|
+
const engines = controller.engines;
|
|
17
|
+
|
|
18
|
+
const [form, setForm] = React.useState({
|
|
19
|
+
input: '', output: '', p12: '', passphrase: '', preset: '',
|
|
20
|
+
name: '', location: '', contact: '', reason: '',
|
|
21
|
+
engine: 'auto', restrict: false, stampImage: '', page: '', box: '',
|
|
22
|
+
...initialData,
|
|
23
|
+
});
|
|
24
|
+
const [errors, setErrors] = React.useState({});
|
|
25
|
+
const [busy, setBusy] = React.useState(false);
|
|
26
|
+
|
|
27
|
+
const set = (k) => (e) => {
|
|
28
|
+
const v = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
|
29
|
+
setForm((f) => ({ ...f, [k]: v }));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const buildPayload = (f) => ({
|
|
33
|
+
...f,
|
|
34
|
+
page: f.page === '' ? undefined : Number(f.page),
|
|
35
|
+
box: parseBox(f.box),
|
|
36
|
+
permissions: f.restrict ? {} : undefined,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const submit = async (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
const payload = buildPayload(form);
|
|
42
|
+
const v = validateSignForm(payload, { engines, t });
|
|
43
|
+
if (!v.valid) { setErrors(v.errors); return; }
|
|
44
|
+
setErrors({});
|
|
45
|
+
setBusy(true);
|
|
46
|
+
try {
|
|
47
|
+
const res = await controller.submit(payload);
|
|
48
|
+
if (!res.ok) { setErrors(res.errors); return; }
|
|
49
|
+
onSigned?.(res.result);
|
|
50
|
+
} finally {
|
|
51
|
+
setBusy(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const field = (key, label, opts = {}) => h('label', { className: 'mosta-pdf-sign-field' },
|
|
56
|
+
h('span', null, t(label)),
|
|
57
|
+
h('input', { type: opts.type || 'text', value: form[key] ?? '', onChange: set(key), placeholder: opts.placeholder }),
|
|
58
|
+
errors[key] && h('span', { className: 'err' }, errors[key]),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return h('form', { onSubmit: submit, className: 'mosta-pdf-sign-form' },
|
|
62
|
+
h(EngineBadge, { engines, t }),
|
|
63
|
+
|
|
64
|
+
// Document (préréglage) — motif + permissions par défaut
|
|
65
|
+
h('label', { className: 'mosta-pdf-sign-field' },
|
|
66
|
+
h('span', null, t('Type de document')),
|
|
67
|
+
h('select', { value: form.preset, onChange: set('preset') },
|
|
68
|
+
h('option', { value: '' }, t('— libre —')),
|
|
69
|
+
presets.map((p) => h('option', { key: p.key, value: p.key }, p.label)),
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
|
|
73
|
+
field('input', 'PDF à signer', { placeholder: 'document.pdf' }),
|
|
74
|
+
field('output', 'PDF de sortie (optionnel)', { placeholder: 'document.signe.pdf' }),
|
|
75
|
+
field('p12', 'Certificat PKCS#12', { placeholder: 'cert.p12' }),
|
|
76
|
+
field('passphrase', 'Phrase secrète', { type: 'password' }),
|
|
77
|
+
|
|
78
|
+
field('reason', 'Motif (sinon celui du document)'),
|
|
79
|
+
field('name', 'Signataire'),
|
|
80
|
+
field('location', 'Lieu'),
|
|
81
|
+
|
|
82
|
+
// Moteur + permissions
|
|
83
|
+
h('label', { className: 'mosta-pdf-sign-field' },
|
|
84
|
+
h('span', null, t('Moteur')),
|
|
85
|
+
h('select', { value: form.engine, onChange: set('engine') },
|
|
86
|
+
h('option', { value: 'auto' }, t('auto')),
|
|
87
|
+
h('option', { value: 'node' }, t('node (PKCS#7)')),
|
|
88
|
+
h('option', { value: 'pyhanko', disabled: !engines.pyhanko }, t('pyhanko (PAdES)')),
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
h('label', { className: 'mosta-pdf-sign-check' },
|
|
92
|
+
h('input', { type: 'checkbox', checked: !!form.restrict, disabled: !engines.canRestrict, onChange: set('restrict') }),
|
|
93
|
+
h('span', null, t('Permissions restrictives (chiffrement AES-256)')),
|
|
94
|
+
),
|
|
95
|
+
errors.permissions && h('span', { className: 'err' }, errors.permissions),
|
|
96
|
+
|
|
97
|
+
// Cachet visible
|
|
98
|
+
field('stampImage', 'Image du cachet (logo, optionnel)', { placeholder: 'logo.png' }),
|
|
99
|
+
field('box', 'Boîte du cachet x1,y1,x2,y2 (optionnel)', { placeholder: '320,40,575,140' }),
|
|
100
|
+
field('page', 'Page du cachet (0 = page 1)', { placeholder: '0' }),
|
|
101
|
+
errors.box && h('span', { className: 'err' }, errors.box),
|
|
102
|
+
|
|
103
|
+
h('button', { type: 'submit', disabled: busy }, busy ? t('Signature…') : t('Signer')),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseBox(s) {
|
|
108
|
+
if (!s || typeof s !== 'string') return undefined;
|
|
109
|
+
const n = s.split(',').map((x) => Number(x.trim()));
|
|
110
|
+
return n.length === 4 && n.every((v) => Number.isFinite(v)) ? n : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default SignForm;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @mostajs/pdf-sign-ui/react — <VerifyPanel> : vérification et affichage des signatures.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { createPdfSignUiController } from '../headless.js';
|
|
5
|
+
|
|
6
|
+
const h = React.createElement;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Vérifie un PDF et affiche le view-model (statut + tableau des signatures).
|
|
10
|
+
* @param {{ service, t?, input?, trustPem?, auto?: boolean }} props
|
|
11
|
+
* `auto` : vérifie automatiquement à l'affichage/à chaque changement d'`input`.
|
|
12
|
+
*/
|
|
13
|
+
export function VerifyPanel({ service, t = (k) => k, input = '', trustPem, auto = false }) {
|
|
14
|
+
const controller = React.useMemo(() => createPdfSignUiController({ service, t }), [service]);
|
|
15
|
+
const [path, setPath] = React.useState(input);
|
|
16
|
+
const [vm, setVm] = React.useState(null);
|
|
17
|
+
const [error, setError] = React.useState(null);
|
|
18
|
+
|
|
19
|
+
const run = React.useCallback((p) => {
|
|
20
|
+
const target = p ?? path;
|
|
21
|
+
if (!target) { setError(t('Indiquer un PDF à vérifier.')); return; }
|
|
22
|
+
setError(null);
|
|
23
|
+
try {
|
|
24
|
+
setVm(controller.verify(target, trustPem ? { trustPem } : {}));
|
|
25
|
+
} catch (e) {
|
|
26
|
+
setError(e?.message || String(e));
|
|
27
|
+
setVm(null);
|
|
28
|
+
}
|
|
29
|
+
}, [controller, path, trustPem, t]);
|
|
30
|
+
|
|
31
|
+
React.useEffect(() => { if (auto && input) run(input); }, [auto, input, run]);
|
|
32
|
+
|
|
33
|
+
const badge = (v) => v === true ? '✓' : v === false ? '✗' : '—';
|
|
34
|
+
|
|
35
|
+
return h('div', { className: 'mosta-pdf-sign-verify' },
|
|
36
|
+
h('div', { className: 'mosta-pdf-sign-verify-bar' },
|
|
37
|
+
h('input', { value: path, onChange: (e) => setPath(e.target.value), placeholder: t('document.signe.pdf') }),
|
|
38
|
+
h('button', { type: 'button', onClick: () => run() }, t('Vérifier')),
|
|
39
|
+
),
|
|
40
|
+
error && h('p', { className: 'err' }, error),
|
|
41
|
+
vm && h('div', { className: `mosta-pdf-sign-verify-result ${vm.signed ? 'signed' : 'unsigned'}` },
|
|
42
|
+
h('p', { className: 'status' }, `${vm.status}${vm.engine ? ` · ${vm.engine}` : ''}`),
|
|
43
|
+
vm.rows.length > 0 && h('table', { className: 'mosta-pdf-sign-verify-table' },
|
|
44
|
+
h('thead', null, h('tr', null,
|
|
45
|
+
h('th', null, t('Champ')), h('th', null, t('Intègre')), h('th', null, t('Valide')),
|
|
46
|
+
h('th', null, t('Confiance')), h('th', null, t('Signataire')), h('th', null, t('Statut')),
|
|
47
|
+
)),
|
|
48
|
+
h('tbody', null, vm.rows.map((r, i) => h('tr', { key: r.field || i },
|
|
49
|
+
h('td', null, r.field),
|
|
50
|
+
h('td', null, badge(r.intact)),
|
|
51
|
+
h('td', null, badge(r.valid)),
|
|
52
|
+
h('td', null, badge(r.trusted)),
|
|
53
|
+
h('td', { className: 'signer' }, r.signer || '—'),
|
|
54
|
+
h('td', null, r.status),
|
|
55
|
+
))),
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
export default VerifyPanel;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// @mostajs/pdf-sign-ui/react — composants React (peer: react, @mostajs/pdf-sign)
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
export { EngineBadge } from './EngineBadge.jsx';
|
|
4
|
+
export { SignForm } from './SignForm.jsx';
|
|
5
|
+
export { VerifyPanel } from './VerifyPanel.jsx';
|