@liorian/sdk 0.51.2-alpha.ee4411f → 1.3.1-alpha.53f9eeb
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/package.json +1 -1
- package/schemas/module.schema.json +265 -16
- package/src/application/service/auth-api-service.ts +26 -0
- package/src/application/service/auth-user.service.ts +52 -0
- package/src/application/service/module-activation-api.service.ts +23 -0
- package/src/application/service/storefront-api.service.ts +4 -1
- package/src/domain/config/__tests__/app.config.test.ts +70 -0
- package/src/domain/config/app.config.ts +86 -25
- package/src/domain/config/auth.config.ts +30 -2
- package/src/domain/entities/module-legal.interface.ts +89 -0
- package/src/domain/entities/module-router.interface.ts +47 -0
- package/src/domain/entities/module-runtime.interface.ts +37 -2
- package/src/domain/entities/module-scope.interface.ts +1 -1
- package/src/domain/entities/module.interface.ts +14 -0
- package/src/infrastructure/module-installer/__tests__/declare-module-from-manifest.test.ts +93 -0
- package/src/infrastructure/module-installer/declare-module-from-manifest.ts +53 -2
- package/src/infrastructure/module-installer/module-legal.api.ts +159 -0
- package/src/infrastructure/module-runtime/__tests__/module-bridge-client.test.ts +12 -0
- package/src/infrastructure/module-runtime/__tests__/module-theme.test.ts +94 -0
- package/src/infrastructure/module-runtime/__tests__/module-transport.test.ts +109 -1
- package/src/infrastructure/module-runtime/bridge-protocol.ts +13 -3
- package/src/infrastructure/module-runtime/module-bootstrap.ts +26 -1
- package/src/infrastructure/module-runtime/module-bridge-client.ts +10 -4
- package/src/infrastructure/module-runtime/module-theme-tokens.ts +265 -0
- package/src/infrastructure/module-runtime/module-theme.ts +100 -0
- package/src/infrastructure/module-runtime/module-transport.ts +46 -7
- package/src/infrastructure/providers/modules-guard.provider.tsx +95 -22
- package/src/infrastructure/stores/__tests__/module-installation.store.test.ts +71 -11
- package/src/infrastructure/stores/__tests__/module.schema.legal.test.ts +129 -0
- package/src/infrastructure/stores/__tests__/module.store.test.ts +409 -0
- package/src/infrastructure/stores/auth-user-connected.store.ts +23 -0
- package/src/infrastructure/stores/module-installation.store.ts +169 -37
- package/src/infrastructure/stores/module.store.ts +230 -99
- package/src/infrastructure/utilities/__tests__/permission.util.test.ts +48 -0
- package/src/infrastructure/utilities/__tests__/socle-api-service.test.ts +164 -0
- package/src/infrastructure/utilities/encryption.service.ts +12 -1
- package/src/infrastructure/utilities/permission.util.ts +27 -0
- package/src/infrastructure/utilities/socle-api-service.ts +92 -32
- package/src/presentation/charts/chart-widgets.types.ts +22 -0
- package/src/presentation/charts/combo-widget.chart-skeleton.tsx +64 -0
- package/src/presentation/charts/combo-widget.chart.tsx +92 -0
- package/src/presentation/charts/index.tsx +2 -0
- package/src/presentation/charts/line-widget.chart.tsx +21 -2
- package/src/presentation/charts/tooltip-widget.chart.tsx +30 -2
- package/src/presentation/components/activity-loader.tsx +1 -1
- package/src/presentation/components/module-runtime/__tests__/module-router.test.ts +74 -0
- package/src/presentation/components/module-runtime/__tests__/use-module-route.test.ts +125 -0
- package/src/presentation/components/module-runtime/declarative/declarative-types.ts +6 -0
- package/src/presentation/components/module-runtime/iframe-host.tsx +99 -18
- package/src/presentation/components/module-runtime/module-router.tsx +201 -0
- package/src/presentation/components/module-runtime/use-module-route.ts +53 -14
- package/src/presentation/data-grid/data-grid-empty.tsx +3 -4
- package/src/presentation/index.tsx +1 -1
- package/src/presentation/module-widget.tsx +6 -231
- package/src/presentation/themes/katon/header-modules-recents.tsx +2 -2
- package/src/presentation/themes/katon/header-tasks-connected-user.tsx +12 -6
- package/src/presentation/themes/katon/helmet-registry.ts +32 -0
- package/src/presentation/themes/katon/index.tsx +2 -0
- package/src/presentation/themes/katon/socle-helmet.tsx +62 -0
- package/src/presentation/themes/katon/view-helmet.tsx +33 -45
- package/src/presentation/widget/fragments-controls.tsx +388 -0
- package/src/presentation/widget/fragments-data.tsx +293 -0
- package/src/presentation/widget/fragments-grids.tsx +137 -0
- package/src/presentation/widget/fragments-lists.tsx +277 -0
- package/src/presentation/widget/fragments-media.tsx +89 -0
- package/src/presentation/widget/fragments-progress.tsx +216 -0
- package/src/presentation/widget/fragments.types.ts +425 -0
- package/src/presentation/widget/fragments.utils.ts +45 -0
- package/src/presentation/widget.tsx +700 -0
package/package.json
CHANGED
|
@@ -59,10 +59,14 @@
|
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
"permissions": [
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"Admin
|
|
62
|
+
"Widget"
|
|
63
|
+
],
|
|
64
|
+
"access": [
|
|
65
|
+
"Admin",
|
|
66
|
+
"Root"
|
|
67
|
+
],
|
|
68
|
+
"userScope": [
|
|
69
|
+
"UserActivity:Get"
|
|
66
70
|
],
|
|
67
71
|
"oauth": {
|
|
68
72
|
"scopes": [
|
|
@@ -330,17 +334,31 @@
|
|
|
330
334
|
"permissions": {
|
|
331
335
|
"type": "array",
|
|
332
336
|
"items": {
|
|
333
|
-
"$ref": "#/definitions/
|
|
337
|
+
"$ref": "#/definitions/module-permission-domain"
|
|
334
338
|
},
|
|
335
339
|
"uniqueItems": true,
|
|
336
|
-
"title": "Permissions",
|
|
337
|
-
"description": "
|
|
340
|
+
"title": "Permissions (domaines exposés)",
|
|
341
|
+
"description": "Domaines de permissions que le module expose et qu'un rôle peut se voir attribuer, sous la forme de domaines PascalCase **sans** préfixe (ex. `Post`, `PostCategory`). Le serveur les compose avec la clé du module (`manifest.key`, ex. `BLOGGING`) pour former la clé namespacée du RBAC — `BLOGGING:Post`. Une liste vide signifie « le module n'expose aucune permission attribuable » ; elle n'empêche pas d'ouvrir le module. Historiquement ce champ portait des couples `<ROLE>:<VERBE>` (`User:Get`, `Editor:Post`) : cette grammaire est supprimée, le RBAC étant dynamique et non plus déclaré par le manifeste.",
|
|
338
342
|
"examples": [
|
|
339
343
|
[
|
|
340
|
-
"
|
|
341
|
-
"
|
|
342
|
-
|
|
343
|
-
|
|
344
|
+
"Post",
|
|
345
|
+
"PostCategory"
|
|
346
|
+
]
|
|
347
|
+
]
|
|
348
|
+
},
|
|
349
|
+
"access": {
|
|
350
|
+
"type": "array",
|
|
351
|
+
"items": {
|
|
352
|
+
"type": "string",
|
|
353
|
+
"pattern": "^[A-Za-z][A-Za-z0-9]*$"
|
|
354
|
+
},
|
|
355
|
+
"uniqueItems": true,
|
|
356
|
+
"title": "Rôles requis pour ouvrir le module",
|
|
357
|
+
"description": "Facultatif. Noms de rôles (`PermissionsConfig`) dont le niveau permet d'ouvrir le module (ex. `Admin`, `Root`). Vide ou absent : aucun palier minimal n'est exigé — seule l'attribution des permissions dans l'écran « Contrôle d'accès » fait foi.",
|
|
358
|
+
"examples": [
|
|
359
|
+
[
|
|
360
|
+
"Admin",
|
|
361
|
+
"Root"
|
|
344
362
|
]
|
|
345
363
|
]
|
|
346
364
|
},
|
|
@@ -380,6 +398,41 @@
|
|
|
380
398
|
]
|
|
381
399
|
]
|
|
382
400
|
},
|
|
401
|
+
"legal": {
|
|
402
|
+
"type": "array",
|
|
403
|
+
"items": {
|
|
404
|
+
"$ref": "#/definitions/legal-document-declaration"
|
|
405
|
+
},
|
|
406
|
+
"title": "Documents légaux (D16)",
|
|
407
|
+
"description": "Conditions d'utilisation, politique de confidentialité et — facultativement — contrat de licence, que l'utilisateur doit accepter avant toute utilisation du module. Le serveur normalise ces déclarations, les empreinte et verrouille l'exécution du module tant que les documents obligatoires ne sont pas acceptés à leur version et à leur empreinte courantes. Un module sans `legal` n'a aucune obligation légale : la porte reste ouverte. Dès qu'un seul document est déclaré, `TERMS` et `PRIVACY` sont tous deux obligatoires ; `LICENSE` peut être déclarée avec `required: false`.",
|
|
408
|
+
"examples": [
|
|
409
|
+
[
|
|
410
|
+
{
|
|
411
|
+
"key": "terms",
|
|
412
|
+
"kind": "TERMS",
|
|
413
|
+
"version": "1.0.0",
|
|
414
|
+
"content": [
|
|
415
|
+
{
|
|
416
|
+
"title": "Article 1 — Objet",
|
|
417
|
+
"paragraphs": [
|
|
418
|
+
"Le présent contrat définit les conditions d'utilisation du module."
|
|
419
|
+
]
|
|
420
|
+
}
|
|
421
|
+
]
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
"key": "privacy",
|
|
425
|
+
"kind": "PRIVACY",
|
|
426
|
+
"version": "1.0.0",
|
|
427
|
+
"content": {
|
|
428
|
+
"Données collectées": {
|
|
429
|
+
"Responsable": "Liorian est responsable du traitement."
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
]
|
|
434
|
+
]
|
|
435
|
+
},
|
|
383
436
|
"oauth": {
|
|
384
437
|
"type": "object",
|
|
385
438
|
"required": [
|
|
@@ -526,6 +579,60 @@
|
|
|
526
579
|
"$ref": "#/definitions/menu",
|
|
527
580
|
"title": "Menu de navigation",
|
|
528
581
|
"description": "Déclaration des éléments de menu du module injectés dans le menu de démarrage (start menu). Les entrées purement programmatiques (`action`) restent dans la déclaration React et ne sont pas représentables en JSON."
|
|
582
|
+
},
|
|
583
|
+
"router": {
|
|
584
|
+
"type": "object",
|
|
585
|
+
"additionalProperties": false,
|
|
586
|
+
"title": "Routeur fichier",
|
|
587
|
+
"description": "Routeur fichier du module (§8.5) : la CLI génère le registre de routes depuis l'arbre de routes (défaut presentation/routes, surcharge router.dir). En mode auto, le manifeste est synchronisé avec les routes (table `routes`, complétion de `menu.items` — les entrées manuelles gagnent). En mode manual (défaut), le routage manuel du module est conservé.",
|
|
588
|
+
"properties": {
|
|
589
|
+
"mode": {
|
|
590
|
+
"type": "string",
|
|
591
|
+
"enum": [
|
|
592
|
+
"auto",
|
|
593
|
+
"manual"
|
|
594
|
+
],
|
|
595
|
+
"title": "Mode",
|
|
596
|
+
"description": "auto : registre généré + manifeste synchronisé. manual : le module gère ses routes à la main."
|
|
597
|
+
},
|
|
598
|
+
"dir": {
|
|
599
|
+
"type": "string",
|
|
600
|
+
"title": "Arbre de routes",
|
|
601
|
+
"description": "Arbre des routes du module, relatif à la racine (défaut presentation/routes)."
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
"routes": {
|
|
606
|
+
"type": "array",
|
|
607
|
+
"title": "Routes dérivées",
|
|
608
|
+
"description": "Table de routes dérivée de l'arbre de routes en mode router.auto (path, title, icon, menu) — écrite par la CLI, ignorée en routage manuel.",
|
|
609
|
+
"items": {
|
|
610
|
+
"type": "object",
|
|
611
|
+
"additionalProperties": false,
|
|
612
|
+
"properties": {
|
|
613
|
+
"path": {
|
|
614
|
+
"type": "string",
|
|
615
|
+
"title": "Chemin",
|
|
616
|
+
"description": "Chemin relatif au module (racine : chaîne vide)."
|
|
617
|
+
},
|
|
618
|
+
"title": {
|
|
619
|
+
"type": "string",
|
|
620
|
+
"title": "Titre"
|
|
621
|
+
},
|
|
622
|
+
"icon": {
|
|
623
|
+
"type": "string",
|
|
624
|
+
"title": "Icône"
|
|
625
|
+
},
|
|
626
|
+
"menu": {
|
|
627
|
+
"type": "boolean",
|
|
628
|
+
"title": "Menu",
|
|
629
|
+
"description": "false : la route n'alimente pas le menu."
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
"required": [
|
|
633
|
+
"path"
|
|
634
|
+
]
|
|
635
|
+
}
|
|
529
636
|
}
|
|
530
637
|
},
|
|
531
638
|
"definitions": {
|
|
@@ -775,16 +882,158 @@
|
|
|
775
882
|
]
|
|
776
883
|
]
|
|
777
884
|
},
|
|
885
|
+
"legal-document-declaration": {
|
|
886
|
+
"type": "object",
|
|
887
|
+
"required": [
|
|
888
|
+
"key",
|
|
889
|
+
"kind",
|
|
890
|
+
"version",
|
|
891
|
+
"content"
|
|
892
|
+
],
|
|
893
|
+
"additionalProperties": false,
|
|
894
|
+
"allOf": [
|
|
895
|
+
{
|
|
896
|
+
"$comment": "`required: false` n'est admis que pour LICENSE. Le déclarer sur TERMS ou PRIVACY au `pack` permet de publier un module que personne n'a l'obligation de lire avant de l'utiliser — le serveur refuserait la publication, autant le dire à l'outil.",
|
|
897
|
+
"if": {
|
|
898
|
+
"properties": {
|
|
899
|
+
"kind": {
|
|
900
|
+
"enum": [
|
|
901
|
+
"TERMS",
|
|
902
|
+
"PRIVACY"
|
|
903
|
+
]
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
"required": [
|
|
907
|
+
"kind"
|
|
908
|
+
]
|
|
909
|
+
},
|
|
910
|
+
"then": {
|
|
911
|
+
"properties": {
|
|
912
|
+
"required": {
|
|
913
|
+
"enum": [
|
|
914
|
+
true
|
|
915
|
+
]
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
],
|
|
921
|
+
"properties": {
|
|
922
|
+
"key": {
|
|
923
|
+
"type": "string",
|
|
924
|
+
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
|
|
925
|
+
"minLength": 2,
|
|
926
|
+
"maxLength": 64,
|
|
927
|
+
"title": "Clé du document",
|
|
928
|
+
"description": "Identifiant stable du document au sein du module (kebab-case). Il entre dans la clé d'acceptation : le renommer ferait redemander l'acceptation.",
|
|
929
|
+
"examples": [
|
|
930
|
+
"terms",
|
|
931
|
+
"privacy",
|
|
932
|
+
"license"
|
|
933
|
+
]
|
|
934
|
+
},
|
|
935
|
+
"kind": {
|
|
936
|
+
"type": "string",
|
|
937
|
+
"enum": [
|
|
938
|
+
"TERMS",
|
|
939
|
+
"PRIVACY",
|
|
940
|
+
"LICENSE"
|
|
941
|
+
],
|
|
942
|
+
"title": "Catégorie du document",
|
|
943
|
+
"description": "`TERMS` (conditions d'utilisation) et `PRIVACY` (politique de confidentialité) sont obligatoires dès qu'un module déclare des documents. `LICENSE` (contrat de licence) est facultative et peut être déclarée avec `required: false`."
|
|
944
|
+
},
|
|
945
|
+
"title": {
|
|
946
|
+
"type": "string",
|
|
947
|
+
"minLength": 1,
|
|
948
|
+
"maxLength": 200,
|
|
949
|
+
"title": "Titre affiché",
|
|
950
|
+
"description": "Titre du document tel que l'utilisateur le lit dans la case à cocher. Par défaut : « Conditions d'utilisation », « Politique de confidentialité » ou « Contrat de licence » selon la catégorie. Fait partie de l'empreinte du contenu."
|
|
951
|
+
},
|
|
952
|
+
"version": {
|
|
953
|
+
"$ref": "#/definitions/semver",
|
|
954
|
+
"title": "Révision du document",
|
|
955
|
+
"description": "Version du document, distincte de celle du module. **Obligatoire** et obligatoire au `pack` : c'est elle qui déclenche la ré-acceptation quand l'éditeur republie ses conditions. Publier des CGU v2 sans changer cette valeur est possible, mais l'empreinte du contenu rattrape la publication et redéclenche l'acceptation."
|
|
956
|
+
},
|
|
957
|
+
"required": {
|
|
958
|
+
"type": "boolean",
|
|
959
|
+
"default": true,
|
|
960
|
+
"title": "Acceptation bloquante",
|
|
961
|
+
"description": "`false` n'est admis que pour `LICENSE` : les conditions d'utilisation et la politique de confidentialité bloquent toujours l'exécution du module."
|
|
962
|
+
},
|
|
963
|
+
"content": {
|
|
964
|
+
"title": "Contenu du document",
|
|
965
|
+
"description": "Le contenu est libre dans sa forme d'écriture — trois notations acceptées, normalisées en une forme canonique unique par le serveur (sections titrées, chacune portant des paragraphes) :\n\n- **tableau** de sections : `[{ \"title\": \"Article 1\", \"paragraphs\": [\"…\", \"…\"] }]` ;\n- **objet** de sections : `{ \"Article 1\": { \"Objet\": \"…\" } }` — l'ordre des clés est l'ordre de lecture ;\n- **Markdown** : une chaîne, découpée sur les titres (`##`) en sections et sur les lignes vides en paragraphes.\n\nL'empreinte SHA-256 est calculée sur la forme canonique, jamais sur la forme d'écriture : réécrire un même CGU du tableau en Markdown ne redéclenche donc pas l'acceptation.",
|
|
966
|
+
"anyOf": [
|
|
967
|
+
{
|
|
968
|
+
"type": "string",
|
|
969
|
+
"minLength": 1,
|
|
970
|
+
"description": "Texte, ou Markdown avec titres `##`."
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
"type": "array",
|
|
974
|
+
"minItems": 1,
|
|
975
|
+
"items": {
|
|
976
|
+
"type": "object",
|
|
977
|
+
"required": [
|
|
978
|
+
"title"
|
|
979
|
+
],
|
|
980
|
+
"additionalProperties": false,
|
|
981
|
+
"properties": {
|
|
982
|
+
"title": {
|
|
983
|
+
"type": "string",
|
|
984
|
+
"minLength": 1
|
|
985
|
+
},
|
|
986
|
+
"paragraphs": {
|
|
987
|
+
"type": "array",
|
|
988
|
+
"minItems": 1,
|
|
989
|
+
"items": {
|
|
990
|
+
"type": "string",
|
|
991
|
+
"minLength": 1
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
"type": "object",
|
|
999
|
+
"minProperties": 1,
|
|
1000
|
+
"additionalProperties": {
|
|
1001
|
+
"oneOf": [
|
|
1002
|
+
{
|
|
1003
|
+
"type": "string",
|
|
1004
|
+
"minLength": 1
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
"type": "object",
|
|
1008
|
+
"additionalProperties": {
|
|
1009
|
+
"type": "string"
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
]
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
]
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
778
1019
|
"role-permission-code": {
|
|
779
1020
|
"type": "string",
|
|
780
1021
|
"pattern": "^[A-Za-z][A-Za-z0-9]*:(Get|Post|Put|Delete)$",
|
|
781
|
-
"title": "
|
|
782
|
-
"description": "
|
|
1022
|
+
"title": "Domaine:verbe (userScope)",
|
|
1023
|
+
"description": "Entrée de `userScope` (D15), au format `<DOMAINE>:<VERBE>`. `DOMAINE` est un nom PascalCase de domaine de permissions (`User`, `UserActivity`, `Organization`, …) et `VERBE` un verbe HTTP (`Get`, `Post`, `Put`, `Delete`). La clé du module n'y figure pas : le périmètre d'un jeton de module reste exprimé en domaines nus.",
|
|
783
1024
|
"examples": [
|
|
784
1025
|
"User:Get",
|
|
785
|
-
"
|
|
786
|
-
|
|
787
|
-
|
|
1026
|
+
"UserActivity:Get"
|
|
1027
|
+
]
|
|
1028
|
+
},
|
|
1029
|
+
"module-permission-domain": {
|
|
1030
|
+
"type": "string",
|
|
1031
|
+
"pattern": "^[A-Za-z][A-Za-z0-9]*$",
|
|
1032
|
+
"title": "Domaine exposé par le module",
|
|
1033
|
+
"description": "Nom PascalCase d'un domaine de permissions exposé par le module (ex. `Post`, `PostCategory`). Le serveur le namespace avec `manifest.key` pour écrire la clé de rôle `<moduleKey>:<Domaine>` (ex. `BLOGGING:Post`).",
|
|
1034
|
+
"examples": [
|
|
1035
|
+
"Post",
|
|
1036
|
+
"PostCategory"
|
|
788
1037
|
]
|
|
789
1038
|
},
|
|
790
1039
|
"module-artifact": {
|
|
@@ -9,6 +9,14 @@ import {
|
|
|
9
9
|
UserAuthSessionCheckingResponseInterface
|
|
10
10
|
} from "../../domain/entities/user-auth.interface";
|
|
11
11
|
import {FetchResponseInterface} from "../../domain/typing/response";
|
|
12
|
+
import {type UserPermissionCapabilities} from "../../domain/entities/user.interface";
|
|
13
|
+
|
|
14
|
+
/** Réponse de `GET /access-control/me/permissions` (P0.4). */
|
|
15
|
+
export interface MyPermissionsResponseInterface {
|
|
16
|
+
userId: string;
|
|
17
|
+
organizationId: string;
|
|
18
|
+
permissions: UserPermissionCapabilities;
|
|
19
|
+
}
|
|
12
20
|
import {AuthConfig} from "../../domain/config/auth.config";
|
|
13
21
|
import type {
|
|
14
22
|
AuthFactor,
|
|
@@ -231,6 +239,24 @@ export class AuthApiService extends SocleApiService {
|
|
|
231
239
|
return unwrap(this.get<FetchResponseInterface<UserInfo>>('/oauth/userinfo'));
|
|
232
240
|
}
|
|
233
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Matrice de permissions effective de l'appelant pour l'organisation
|
|
244
|
+
* courante (P0.4).
|
|
245
|
+
*
|
|
246
|
+
* `/oauth/userinfo` ne publie que l'identité OIDC : la matrice d'accès n'y
|
|
247
|
+
* figure pas. Elle est donc lue sur une route dédiée de l'API
|
|
248
|
+
* access-control, recalculée à chaque appel depuis les rôles vivants —
|
|
249
|
+
* condition indispensable pour que l'image rechargée remplace réellement
|
|
250
|
+
* l'instantané figé au login.
|
|
251
|
+
*
|
|
252
|
+
* L'organisation est portée par l'en-tête `X-Organization-Id` (voir
|
|
253
|
+
* `SocleApiService.buildAuthHeaders`) : elle correspond donc toujours à
|
|
254
|
+
* l'organisation courante de la session.
|
|
255
|
+
*/
|
|
256
|
+
static async myPermissions(): Promise<MyPermissionsResponseInterface> {
|
|
257
|
+
return unwrap(this.get<FetchResponseInterface<MyPermissionsResponseInterface>>('/access-control/me/permissions'));
|
|
258
|
+
}
|
|
259
|
+
|
|
234
260
|
static async listConsents(): Promise<AuthorizedApplication[]> {
|
|
235
261
|
return unwrap(this.get<FetchResponseInterface<AuthorizedApplication[]>>('/oauth/user/consents'));
|
|
236
262
|
}
|
|
@@ -114,6 +114,58 @@ export class AuthUserService {
|
|
|
114
114
|
await instance.set('apiKey', apiKey).save();
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Rafraîchit la matrice de permissions de la session (P0.4).
|
|
119
|
+
*
|
|
120
|
+
* Les permissions de l'utilisateur sont aplaties au moment de la connexion
|
|
121
|
+
* puis plus jamais relues : l'image reste celle de l'instant du login. Toute
|
|
122
|
+
* mutation d'accès survenue ensuite — installation d'un module, mise à jour
|
|
123
|
+
* d'un rôle, changement d'organisation — est donc invisible de l'interface
|
|
124
|
+
* jusqu'à la déconnexion suivante. Le garde d'accès des modules évalue cette
|
|
125
|
+
* image : l'utilisateur garde un « Accès refusé » sur un module qu'il vient
|
|
126
|
+
* d'installer, ou accède à un écran devenu interdit.
|
|
127
|
+
*
|
|
128
|
+
* Cette méthode recalcule `user.permissions` depuis l'API et réécrit la
|
|
129
|
+
* session chiffrée. **Best-effort** : un échec réseau ne doit pas
|
|
130
|
+
* interrompre une installation réussie ni un changement d'organisation — la
|
|
131
|
+
* matrice reste simplement à jour au prochain appel. L'erreur est donc
|
|
132
|
+
* avalée et non propagée : l'appelant n'a pas à traiter un échec
|
|
133
|
+
* d'affichage comme un échec de l'action métier qu'il vient de réussir.
|
|
134
|
+
*
|
|
135
|
+
* L'organisation courante doit être déjà positionnée dans la session :
|
|
136
|
+
* l'appel lit l'en-tête `X-Organization-Id` (voir `buildAuthHeaders`), donc
|
|
137
|
+
* un rafraîchissement avant le choix d'organisation viserait le mauvais
|
|
138
|
+
* périmètre — il est alors ignoré plutôt que d'écrire une matrice vide.
|
|
139
|
+
*
|
|
140
|
+
* @returns la matrice réécrite, ou `null` si aucun rafraîchissement n'a eu lieu.
|
|
141
|
+
*/
|
|
142
|
+
static async refreshPermissions(): Promise<UserInterface['permissions'] | null> {
|
|
143
|
+
try {
|
|
144
|
+
await this.ready();
|
|
145
|
+
|
|
146
|
+
// Sans session ni organisation courante, il n'y a rien à recalculer.
|
|
147
|
+
if (!instance.get('token') || !instance.get('currentOrganization')) return null;
|
|
148
|
+
|
|
149
|
+
const currentUser = instance.get('user');
|
|
150
|
+
if (!currentUser) return null;
|
|
151
|
+
|
|
152
|
+
// Import différé : `auth-api-service` lit la session via cette classe,
|
|
153
|
+
// et un import statique refermerait le cycle au chargement du module.
|
|
154
|
+
const {AuthApiService} = await import("./auth-api-service");
|
|
155
|
+
const result = await AuthApiService.myPermissions();
|
|
156
|
+
|
|
157
|
+
// Une réponse sans matrice exploitable ne doit pas effacer l'image
|
|
158
|
+
// courante : `{}` est une matrice valide (aucun accès), `undefined`
|
|
159
|
+
// ne l'est pas.
|
|
160
|
+
if (!result?.permissions || typeof result.permissions !== "object") return null;
|
|
161
|
+
|
|
162
|
+
await this.setUser({...currentUser, permissions: result.permissions});
|
|
163
|
+
return result.permissions;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
117
169
|
/**
|
|
118
170
|
* Installe une session neuve (connexion directe ou échange du code OAuth).
|
|
119
171
|
*
|
|
@@ -43,6 +43,29 @@ export class ModuleActivationApiService extends SocleApiService {
|
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Garantir une activation active pour un module (P0.1).
|
|
48
|
+
*
|
|
49
|
+
* Action **idempotente** : crée l'activation si elle est absente, réactive
|
|
50
|
+
* une suspension, et ne fait rien si le module est déjà actif. Elle ne
|
|
51
|
+
* consomme aucune clé sérielle.
|
|
52
|
+
*
|
|
53
|
+
* Sert au socle pour un module first-party (`external: false`), qui ne
|
|
54
|
+
* passe pas par la chaîne d'installation : sans elle,
|
|
55
|
+
* `ModuleAccessMiddleware` refuse **toutes** les routes métier du module
|
|
56
|
+
* avec « le module X n'est pas activé pour cette organisation ».
|
|
57
|
+
*
|
|
58
|
+
* Elle n'installe rien : aucun téléchargement, aucune vérification
|
|
59
|
+
* d'artefact. Un produit éditeur distribué avec `external: true` doit
|
|
60
|
+
* toujours passer par `ModuleInstallerService.install`.
|
|
61
|
+
*/
|
|
62
|
+
static async ensureActivation(organizationId: string, moduleIdentifier: string) {
|
|
63
|
+
return await this.post<FetchResponseInterface<ModuleActivationInterface>>(
|
|
64
|
+
`/module-activation/organizations/${organizationId}/modules/${moduleIdentifier}/ensure`,
|
|
65
|
+
{}
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
/** Désactiver un module pour une organisation */
|
|
47
70
|
static async deactivateModule(organizationId: string, moduleIdentifier: string) {
|
|
48
71
|
return await this.delete(
|
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
*/
|
|
20
20
|
export class StorefrontApiService extends SocleApiService {
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/** Hôte de `liorian-api-store`, relu à chaque appel (cf. `SocleApiService.baseUrl`). */
|
|
23
|
+
protected static get baseUrl(): string {
|
|
24
|
+
return ((AppConfig.STORE_API_HOST || AppConfig.CORE_API_HOST) ?? "") as string;
|
|
25
|
+
}
|
|
23
26
|
|
|
24
27
|
// ─── Catalogue : modules ────────────────────────────────────────
|
|
25
28
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lecture d'environnement de `app.config` (D1).
|
|
5
|
+
*
|
|
6
|
+
* Deux états doivent rester distinguables : le socle, où le bundler inline les
|
|
7
|
+
* valeurs, et le bundle de module, où `process` n'existe pas. Une lecture
|
|
8
|
+
* par `typeof process` n'est inoffensive qu'aussi longtemps qu'elle s'appuie
|
|
9
|
+
* sur le polyfill interne de Next : `CORE_API_HOST` redevenu
|
|
10
|
+
* `undefined` suffisait à faire partir le déverrouillage sur `/m/undefined/…`.
|
|
11
|
+
*/
|
|
12
|
+
const loadConfig = async (env: Record<string, string | undefined>) => {
|
|
13
|
+
vi.resetModules()
|
|
14
|
+
const previous = {...process.env}
|
|
15
|
+
for (const [key, value] of Object.entries(env)) {
|
|
16
|
+
if (value === undefined) delete process.env[key]
|
|
17
|
+
else process.env[key] = value
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return (await import("../app.config")).AppConfig
|
|
21
|
+
} finally {
|
|
22
|
+
for (const key of Object.keys(process.env)) delete process.env[key]
|
|
23
|
+
Object.assign(process.env, previous)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('AppConfig — lecture module-safe', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.resetModules()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.restoreAllMocks()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('lit les hôtes exposés par l\'application', async () => {
|
|
37
|
+
const config = await loadConfig({
|
|
38
|
+
NEXT_PUBLIC_CORE_API_HOST: 'https://localhost:5711/api',
|
|
39
|
+
NEXT_PUBLIC_APP_HOST: 'https://localhost:5010',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(config.CORE_API_HOST).toBe('https://localhost:5711/api')
|
|
43
|
+
expect(config.APP_HOST).toBe('https://localhost:5010')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('retombe sur les valeurs par défaut quand l\'environnement est vide', async () => {
|
|
47
|
+
const config = await loadConfig({
|
|
48
|
+
NEXT_PUBLIC_CORE_API_HOST: undefined,
|
|
49
|
+
NEXT_PUBLIC_UI_ACTIVITY_TRACKER_TIMEOUT: undefined,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(config.CORE_API_HOST).toBeUndefined()
|
|
53
|
+
expect(config.UI_ACTIVITY_TRACKER_TIMEOUT).toBe(3 * 60 * 1000)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('ne lève pas quand `process` est absent — le cas d\'un bundle de module (D1)', async () => {
|
|
57
|
+
vi.resetModules()
|
|
58
|
+
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'process')
|
|
59
|
+
// Simule l'absence de `process` : un bundle de module n'injecte que
|
|
60
|
+
// `process.env.NODE_ENV` via esbuild, `process` lui-même reste indéfini.
|
|
61
|
+
Object.defineProperty(globalThis, 'process', {value: undefined, configurable: true})
|
|
62
|
+
try {
|
|
63
|
+
const {AppConfig} = await import("../app.config")
|
|
64
|
+
expect(AppConfig.CORE_API_HOST).toBeUndefined()
|
|
65
|
+
expect(AppConfig.UI_ACTIVITY_TRACKER_TIMEOUT).toBe(3 * 60 * 1000)
|
|
66
|
+
} finally {
|
|
67
|
+
if (descriptor) Object.defineProperty(globalThis, 'process', descriptor)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
})
|