27 Commits

Author SHA1 Message Date
ae68de8a3c Merge pull request 'feat: Update avatar URLs in Avatar and LoginModal components' (#68) from fix/change_default_avatar_urls into main
Le service que j'avais utilisé pour générer des avatars aléatoires pour cette démo a cessé de fonctionner aujourd'hui ... XD

Je le remplace donc par un autre service ^^

Reviewed-on: P4Pillon/Krys4lide#68
Reviewed-by: kosssi <simon@p4pillon.org>
2024-10-14 17:11:13 +02:00
e24b9c7859 feat: Update avatar URLs in Avatar and LoginModal components 2024-10-14 17:10:46 +02:00
c83824ae34 Merge pull request 'Initialisation de la crate FSV, couche haut-niveau des accès aux fonctions SSV' (#71) from 38-fsv-high-level-lib into main
### Détails

- Création de la crate fsv, couche de haut niveau pour l'usage des librairies FSV
- Implémentation partielle des appels "haut niveau" aux fonctions SSV_InitLIB2, SSV_LireCartePS et SSV_LireConfig
- Implémentation de la gestion des erreurs numériques de la librairie C pour ces fonctions

### Pourquoi ?

L'usage est de séparer les couches bas niveau, exposant des versions légèrement "rustifiées" des appels aux fonctions des librairies C, des couches "haut niveau", garantissant un usage "safe" et "user friendly".

Contribue à #38
Closes #49

Reviewed-on: P4Pillon/Krys4lide#71
Reviewed-by: kosssi <simon@p4pillon.org>
2024-10-09 22:46:41 +02:00
1b94fefad3 fixup! feat: implémentation partielle de la fonction get_config et de ses erreurs 2024-10-09 22:45:01 +02:00
5f7229c307 fixup! feat: Implémentation de la gestion des erreurs numériques de la librairie C pour la fonction InitLIB2 2024-10-09 22:43:25 +02:00
d043915a29 feat: implémentation partielle de la fonction get_config et de ses erreurs 2024-10-09 22:38:53 +02:00
2260b0cfa8 feat: implement LireCartePS with hardcoded reader and all errors 2024-10-09 22:38:53 +02:00
203521fe01 feat: Implémentation de la gestion des erreurs numériques de la librairie C pour la fonction InitLIB2
Co-authored-by: theo <t.lettermann@criteo.com>
2024-10-09 22:38:53 +02:00
3c1e691cb8 feat: Création de la crate fsv, couche de haut niveau pour l'usage des librairies FSV 2024-10-09 22:38:53 +02:00
add40f32c5 Merge pull request 'Création d'une sys-crate pour la gestion des librairies FSV' (#70) from feat/38-fsv-sys-crate into main
### Détails

Début d'implémentation de bindings pour FSV SESAM-Vitale

- Création de la crates/fsv-sys
- Ajout des headers des versions FSV 1.40.14 et 1.40.13 dans un sous-dossier crates/fsv-sys/vendor
- Génération des bindings depuis ces headers avec bindgen
- Implémentation d'une structure de loading de la librairie au runtime
    - Un pattern similaire au "TypeState pattern" est utilisé pour gérer plusieurs versions possibles des bindings FSV
    - Une macro permet de générer avec un peu moins de boilerplate que nécessaire la couche d'accès aux fonctions de la librairie

### Pourquoi ?

Cette PR est une étape importante du ticket #38

Une telle sys-crate, respectant (à peu près) les bonnes pratiques d'une telle implem, permet :
- Pouvoir être diffusée publiquement en tant que telle, pour faciliter le travail à des copaines
- De concentrer le travail très spécifique et bas niveau de gestion des librairies et des bindings dans une crate dédiée
- De ne pas "forcer" une approche "orientée" sur l'API plus haut niveau qu'on décide de brancher sur ces bindings
    - En effet, l'implem haut niveau fait des choix non neutres, comme le choix de certains types, la technique de gestion des erreurs, etc.

### Documentation

# Aide reçue sur les forums Rust
- [Génération des bindings avec Bindgen](https://users.rust-lang.org/t/how-to-handle-bindgen-generating-types-aliases-instead-of-callable-functions/118083)
- [Gestion des versions multiples avec la structure de loading de la librairie](https://users.rust-lang.org/t/manage-various-versions-of-a-c-library-loaded-at-runtime/118973)

# Documentations
- [Making a sys-crate](https://kornel.ski/rust-sys-crate)
- [LibLoading](https://docs.rs/libloading/latest/libloading/)
- [Bindgen](https://rust-lang.github.io/rust-bindgen/)

Reviewed-on: P4Pillon/Krys4lide#70
Reviewed-by: kosssi <simon@p4pillon.org>
2024-10-09 22:37:36 +02:00
d8b8ce9a77 feat: improve the fsv-sys README, and add a PROGESS.md for implementation tracking 2024-10-09 22:31:26 +02:00
9997ee43f8 feat: Gestion des versions multiples de FSV dans le wrapper exposant les fonctions de la librairie 2024-10-09 22:31:26 +02:00
4ab8a1de81 feat: handle multi-version bindings generation 2024-10-09 22:31:26 +02:00
d13f36c5e2 feat: Première implémentation de bindings pour FSV SESAM-Vitale
- Création de la crates/fsv-sys
- Ajout des headers de la FSV 1.40.14.13 dans crates/fsv-sys/vendor
- Génération des bindings depuis ces headers avec bindgen
- Implémentation d'une structure de loading de la librairie au runtime
- Implémentation d'une macro permettant de générer facilement la couche d'accès aux fonctions de la librairie
2024-10-09 22:31:26 +02:00
922598415c Merge pull request 'Setup de SeaORM + SQLite comme base de données' (#64) from feat/9_setup_db into main
### Détails

Comme défini dans le ticket #9 j'ai mis en place l'ORM "SeaORM" et une base de donnée en SQLite.

La PR contient également quelques modif annexes dont j'avais besoin :
- Un petit fix dans l'usage de get_router
- Une amélioration du système de chargement des fichiers de config (migration de ce module de anyhow à thiserror et système pour éviter les initialisations multiples)
- L'ajout d'un onglet "DEBUG" dans l'interface graphique, pour ajouter un exemple d'intéraction (Read & Write) avec la base de données

Il reste un comportement étrange : lorsqu'on érit dans la base de donnée, ça trigger l'autoreload/hotreload. Je n'ai pas cherché à résoudre le bug, car la suppression de HTMx devrait chambouler tout ça, donc ça ne me parait pas utile d'y passer du temps.

Closes #9

### Documentation

J'ai principalement suivi la documentation d'installation de SeaORM : https://www.sea-ql.org/SeaORM/docs/index/

Reviewed-on: P4Pillon/Krys4lide#64
Reviewed-by: kosssi <simon@p4pillon.org>
2024-10-02 12:04:04 +02:00
5f20c4b893 fix: limit hot-reload to usefull situations 2024-09-24 18:33:54 +02:00
2ded18692d feat: add a debug page calling database debug functions from the backend 2024-09-24 17:55:34 +02:00
8700354ad2 feat: fix CORS 2024-09-24 17:54:02 +02:00
0aa0aebbad chore: Remove app crate 2024-09-24 17:39:57 +02:00
f3e2090e7f chore: fixup "define workspace.dependencies and add sea-orm-cli as dev-dep" 2024-09-24 15:52:43 +02:00
90e79c1fa4 feat: fixup "add a DEBUG page to the UI with a database usage example" 2024-09-24 15:52:28 +02:00
777b7f2425 feat: fixup "setup seaorm and a first "debug" entity as example" 2024-09-24 15:52:08 +02:00
fcba21ef68 chore: define workspace.dependencies and add sea-orm-cli as dev-dep 2024-09-24 13:49:50 +02:00
0d51e3aa68 feat: add a DEBUG page to the UI with a database usage example 2024-09-24 13:49:50 +02:00
d43ee1c28f feat: setup seaorm and a first "debug" entity as example 2024-09-24 13:49:50 +02:00
2ef527fa64 feat: add a function to init properly app library 2024-09-24 13:49:50 +02:00
502dc6f77d feat: migrate utils::config from anyhow to thiserror and handle a "single config init" mechanism 2024-09-24 13:05:27 +02:00
77 changed files with 3175 additions and 2160 deletions

View File

@ -1,2 +1,3 @@
SESAM_FSV_VERSION=1.40.13 SESAM_FSV_VERSION=1.40.13
SESAM_INI_PATH=/etc/opt/santesocial/fsv/${SESAM_FSV_VERSION}/conf/sesam.ini SESAM_INI_PATH=/etc/opt/santesocial/fsv/${SESAM_FSV_VERSION}/conf/sesam.ini
DATABASE_URL=sqlite://p4pillon.sqlite?mode=rwc

View File

@ -1,2 +1,3 @@
SESAM_FSV_VERSION=1.40.13 SESAM_FSV_VERSION=1.40.13
SESAM_INI_PATH=${ALLUSERSPROFILE}\\santesocial\\fsv\\${SESAM_FSV_VERSION}\\conf\\sesam.ini SESAM_INI_PATH=${ALLUSERSPROFILE}\\santesocial\\fsv\\${SESAM_FSV_VERSION}\\conf\\sesam.ini
DATABASE_URL=sqlite://p4pillon.sqlite?mode=rwc

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
Cargo.lock -merge linguist-generated=false

3
.gitignore vendored
View File

@ -23,3 +23,6 @@ target/
# Ignore .env files # Ignore .env files
.env .env
# Development Database
*.sqlite

2334
Cargo.lock

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,18 @@ members = [
"crates/backend", "crates/backend",
"crates/desktop", "crates/desktop",
"crates/sesam-vitale", "crates/sesam-vitale",
"crates/fsv",
"crates/fsv-sys", "crates/fsv-sys",
"crates/utils", "crates/utils",
"migration",
"entity",
".",
] ]
[workspace.dependencies]
anyhow = "1.0"
dotenv = "0.15"
sea-orm-cli = "1.0.1"
sea-orm = "1.0.1"
serde = { version = "1.0.210", features = ["derive"] }
thiserror = "1.0"

View File

@ -44,6 +44,24 @@ La CLI Tauri est nécessaire au lancement du client `desktop`. Elle peut être i
cargo install tauri-cli --version "^2.0.0-rc" cargo install tauri-cli --version "^2.0.0-rc"
``` ```
#### SeaORM CLI
SeaORM est notre ORM. Le CLI SeaORM est nécessaire pour la génération des modèles de la base de données et des migrations associées. Elle peut être installée via Cargo :
```bash
cargo install sea-orm-cli
```
L'applicatif va chercher les informations de connexion à la base de données dans la variable `DATABASE_URL` importée depuis les [fichiers de configuration](#fichiers-de-configuration).
```.env
DATABASE_URL=sqlite://p4pillon.sqlite?mode=rwc
```
Toutefois, l'usage de la CLI de SeaORM nécessite de renseigner les informations de connexion à la base de données dans un fichier `.env` situé à la racine du projet.
> Astuce : utilisé un lien symbolique pour éviter de dupliquer le fichier `.env`.
#### FSV-sys #### FSV-sys
La crate `fsv-sys` nécessite la présence des librairies fournies par le package FSV et la CryptolibCPS. Les instructions d'installation sont disponibles dans le [README](crates/sesam-vitale/README.md) de la crate `fsv-sys`. La crate `fsv-sys` nécessite la présence des librairies fournies par le package FSV et la CryptolibCPS. Les instructions d'installation sont disponibles dans le [README](crates/sesam-vitale/README.md) de la crate `fsv-sys`.
@ -58,7 +76,7 @@ Pour lancer l'application en mode développement, il est nécessaire d'exécuter
```bash ```bash
# Lancement du serveur backend # Lancement du serveur backend
systemfd --no-pid -s http::3030 -- cargo watch -x 'run --bin backend' systemfd --no-pid -s http::8080 -- cargo watch -w crates/backend -x 'run --bin backend'
``` ```
```bash ```bash
@ -66,9 +84,13 @@ systemfd --no-pid -s http::3030 -- cargo watch -x 'run --bin backend'
# - frontend (serveur web, accessible via navigateur) # - frontend (serveur web, accessible via navigateur)
bun run --cwd frontend/ dev bun run --cwd frontend/ dev
# - desktop (client desktop, basé sur Tauri) # - desktop (client desktop, basé sur Tauri)
cargo tauri dev cargo tauri dev --no-watch
``` ```
> Pour circonscrire les hot-reloads intempestifs mais peu utiles :
> - le `backend` n'est rechargé que si des modifications sont détectées dans le dossier précisé par `-w crates/backend`
> - le rechargement du `desktop` est désactivé par l'option `--no-watch` ; en effet, le rechargement du `frontend` est déjà pris en charge par `bun` et ne nécessite pas de rechargement du `desktop`
## Build ## Build
Pour packager le client `desktop`, il est nécessaire de faire appel à la CLI Tauri, qui se charge de gérer le build du `frontend` et son intégration au bundle : Pour packager le client `desktop`, il est nécessaire de faire appel à la CLI Tauri, qui se charge de gérer le build du `frontend` et son intégration au bundle :
@ -76,3 +98,25 @@ Pour packager le client `desktop`, il est nécessaire de faire appel à la CLI T
```bash ```bash
cargo tauri build cargo tauri build
``` ```
## Gestion de la base de données
### Création d'une migration
```bash
sea-orm-cli migrate generate <nom_de_la_migration>
```
Cette commande génère un fichier de migration à adapter dans le dossier `migration/src`.
### Appliquer les migrations
```bash
sea-orm-cli migrate up
```
### Génération des entitées
```bash
sea-orm-cli generate entity -o entity/src/entities --with-serde both
```

View File

@ -1,4 +0,0 @@
/target
# Tailwind CSS CLI
tailwindcss

View File

@ -1,21 +0,0 @@
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[dependencies]
askama = "0.12.1"
askama_axum = "0.4.0"
axum = "0.7.5"
axum-htmx = { version = "0.6", features = ["auto-vary"] }
listenfd = "1.0.1"
notify = "6.1.1"
serde = { version = "1.0.204", features = ["derive"] }
thiserror = "1.0.63"
tokio = { version = "1.39.1", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.5.2", features = ["fs"] }
tower-livereload = "0.9.3"
[dev-dependencies]
cargo-watch = "8.5.1"
systemfd = "0.4.0"

View File

@ -1,35 +0,0 @@
## Pré-requis
- Récupérer le binaire TailwindCSS : https://tailwindcss.com/blog/standalone-cli
## Exécution
- Lancer tailwindcss en mode watch dans un terminal :
```bash
./tailwindcss -i css/input.css -o assets/css/style.css --watch
```
- Lancer le serveur web dans un autre terminal :
```bash
cargo run --bin app
```
## Rechargement automatique (_auto-reload_)
Pour le projet `app`, nous utilisons en plus de `cargo-watch` ses librairies :
- [`systemfd`](https://github.com/mitsuhiko/systemfd) permet de redémarrer un serveur sans interrompre les connexions en cours, il transmet le descripteur de fichier du socket à une nouvelle instance du serveur (exemple: `cargo watch -x run` --> `systemfd --no-pid -s http::3000 -- cargo watch -x run`). Si le port est déjà pris il en prendra un autre.
- [`listenfd`](https://github.com/mitsuhiko/listenfd) permet, côté _Rust_, de démarrer un serveur en utilisant des connexions déjà ouvertes.
Pour notre application voici la commande à lancer :
```bash
systemfd --no-pid -s http::3000 -- cargo watch -x 'run --bin app'
```
## Chargement à chaud (_livereload_)
Pour que notre navigateur rafraîchisse automatique notre page lorsque le serveur a été recompilé, nous utilisons la librairie [`tower-livereload`](https://github.com/leotaku/tower-livereload).
A chaque changement, que ça soit sur du code en _Rust_, _HTML_, _CSS_ ou _JS_ alors le navigateur va recharger entièrement la page.
En Rust, il n'existe pas encore d'outil de _Hot Reload_ complet et intégré comme on en trouve dans d'autres environnements de développement web, comme pour _Node.js_.

View File

@ -1,6 +0,0 @@
[general]
# Directories to search for templates, relative to the crate root.
dirs = [
"src/pages",
"src/components",
]

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -1,23 +0,0 @@
{% if hx_request %}
<title>{% block title %}{{ title }}{% endblock %}</title>
{% block body %}{% endblock %}
{% else %}
<!doctype html>
<html lang="fr" class="h-full">
<head>
<title>{% block title %}{{ title }}{% endblock %}</title>
<script src="/assets/js/htmx@2.0.1.min.js"></script>
<script src="/assets/js/alpinejs@3.14.1.min.js" defer></script>
<script src="/assets/js/flowbite@2.5.1.min.js"></script>
<link href="/assets/css/style.css" rel="stylesheet">
{% block head %}{% endblock %}
</head>
<body class="h-full">
<div class="min-h-full">
{% block body %}{% endblock %}
</div>
</body>
</html>
{% endif %}

View File

@ -1,18 +0,0 @@
{% set selected = item.id == current %}
<li>
<a
href="{{ item.href }}"
{% if selected -%}
class="block py-2 px-3 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500"
aria-current="page"
{% else -%}
class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
{% endif -%}
hx-get="{{ item.href }}"
hx-push-url="true"
hx-swap="outerHTML"
hx-select-oob="#menu-items,#page-header,#page-main"
>
{{ item.label }}
</a>
</li>

View File

@ -1,50 +0,0 @@
{% macro navbar(current) %}
{% let items=crate::menu::get_menu_items() %}
<nav class="bg-white border-gray-200 dark:bg-gray-900">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
<img src="https://flowbite.com/docs/images/logo.svg" class="h-8" alt="Flowbite Logo" />
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">Krys4lide</span>
</a>
<div class="flex items-center md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
<button type="button" class="flex text-sm bg-gray-800 rounded-full md:me-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="sr-only">Ouvrir le menu de profil</span>
<img class="w-8 h-8 rounded-full" src="https://flowbite.com/docs/images/people/profile-picture-3.jpg" alt="user photo">
</button>
<!-- Dropdown menu -->
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-dropdown">
<div class="px-4 py-3">
<span class="block text-sm text-gray-900 dark:text-white">Bonnie Green</span>
<span class="block text-sm text-gray-500 truncate dark:text-gray-400">name@flowbite.com</span>
</div>
<ul class="py-2" aria-labelledby="user-menu-button">
<li>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Profile</a>
</li>
<li>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Settings</a>
</li>
<li>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Sign out</a>
</li>
</ul>
</div>
<button data-collapse-toggle="navbar-user" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-user" aria-expanded="false">
<span class="sr-only">Ouvrir le menu de navigation</span>
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
</svg>
</button>
</div>
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
<ul id="menu-items" class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
{% for item in items %}
{% include "navbar/menu-item.html" %}
{% endfor %}
</ul>
</div>
</div>
</nav>
{% endmacro %}

View File

@ -1,22 +0,0 @@
<div role="status" class="animate-pulse max-w-sm p-4 border border-gray-200 rounded shadow md:p-6 dark:border-gray-700">
<div class="flex items-center justify-center h-48 mb-4 bg-gray-300 rounded dark:bg-gray-700">
<svg class="w-10 h-10 text-gray-200 dark:text-gray-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 20">
<path d="M14.066 0H7v5a2 2 0 0 1-2 2H0v11a1.97 1.97 0 0 0 1.934 2h12.132A1.97 1.97 0 0 0 16 18V2a1.97 1.97 0 0 0-1.934-2ZM10.5 6a1.5 1.5 0 1 1 0 2.999A1.5 1.5 0 0 1 10.5 6Zm2.221 10.515a1 1 0 0 1-.858.485h-8a1 1 0 0 1-.9-1.43L5.6 10.039a.978.978 0 0 1 .936-.57 1 1 0 0 1 .9.632l1.181 2.981.541-1a.945.945 0 0 1 .883-.522 1 1 0 0 1 .879.529l1.832 3.438a1 1 0 0 1-.031.988Z"/>
<path d="M5 5V.13a2.96 2.96 0 0 0-1.293.749L.879 3.707A2.98 2.98 0 0 0 .13 5H5Z"/>
</svg>
</div>
<div class="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
<div class="flex items-center mt-4">
<svg class="w-10 h-10 me-3 text-gray-200 dark:text-gray-700" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 0a10 10 0 1 0 10 10A10.011 10.011 0 0 0 10 0Zm0 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm0 13a8.949 8.949 0 0 1-4.951-1.488A3.987 3.987 0 0 1 9 13h2a3.987 3.987 0 0 1 3.951 3.512A8.949 8.949 0 0 1 10 18Z"/>
</svg>
<div>
<div class="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-32 mb-2"></div>
<div class="w-48 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
</div>
</div>
<span class="sr-only">Loading...</span>
</div>

View File

@ -1,4 +0,0 @@
<div role="status" class="animate-pulse flex items-center justify-center h-full">
<div class="w-32 h-4 bg-gray-200 rounded-full dark:bg-gray-700 me-3"></div>
<div class="w-32 h-4 bg-gray-200 rounded-full dark:bg-gray-700"></div>
</div>

View File

@ -1 +0,0 @@
<div role="status" class="animate-pulse h-7 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mt-3"></div>

View File

@ -1,21 +0,0 @@
use std::path::PathBuf;
use axum::http::{StatusCode, Uri};
use axum_htmx::AutoVaryLayer;
use tower_http::services::ServeDir;
mod menu;
mod pages;
async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
}
pub async fn get_router(assets_path: PathBuf) -> axum::Router<()> {
axum::Router::new()
.nest_service("/assets", ServeDir::new(assets_path))
.merge(pages::get_routes())
.fallback(fallback)
// The AutoVaryLayer is used to avoid cache issues with htmx (cf: https://github.com/robertwayne/axum-htmx?tab=readme-ov-file#auto-caching-management)
.layer(AutoVaryLayer)
}

View File

@ -1,84 +0,0 @@
use std::path::{Path, PathBuf};
use std::{env, io};
use axum::body::Body;
use axum::http::Request;
use listenfd::ListenFd;
use notify::Watcher;
use thiserror::Error;
use tokio::net::TcpListener;
use tower_livereload::predicate::Predicate;
use tower_livereload::LiveReloadLayer;
use ::app::get_router;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Unable to bind to TCP listener")]
TCPListener(#[from] std::io::Error),
#[error("Error with the notify watcher")]
NotifyWatcher(#[from] notify::Error),
#[error("Missing environment variable {var}")]
MissingEnvVar { var: &'static str },
}
/// Nous filtrons les requêtes de `htmx` pour ne pas inclure le script _JS_ qui gère le rechargement
/// Voir https://github.com/leotaku/tower-livereload/pull/3
#[derive(Copy, Clone)]
struct NotHtmxPredicate;
impl<T> Predicate<Request<T>> for NotHtmxPredicate {
fn check(&mut self, req: &Request<T>) -> bool {
!(req.headers().contains_key("hx-request"))
}
}
const DEFAULT_LISTENER: &str = "localhost:3000";
async fn get_tcp_listener() -> Result<TcpListener, io::Error> {
let mut listenfd = ListenFd::from_env();
match listenfd.take_tcp_listener(0)? {
// if we are given a tcp listener on listen fd 0, we use that one
Some(listener) => {
listener.set_nonblocking(true)?;
Ok(TcpListener::from_std(listener)?)
}
// otherwise fall back to local listening
None => Ok(TcpListener::bind(DEFAULT_LISTENER).await?),
}
}
fn get_livereload_layer(
templates_paths: Vec<PathBuf>,
) -> Result<LiveReloadLayer<NotHtmxPredicate>, notify::Error> {
let livereload = LiveReloadLayer::new();
let reloader = livereload.reloader();
let mut watcher = notify::recommended_watcher(move |_| reloader.reload())?;
for templates_path in templates_paths {
watcher.watch(templates_path.as_path(), notify::RecursiveMode::Recursive)?;
}
Ok(livereload.request_predicate::<Body, NotHtmxPredicate>(NotHtmxPredicate))
}
#[tokio::main]
async fn main() -> Result<(), AppError> {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| AppError::MissingEnvVar {
var: "CARGO_MANIFEST_DIR",
})?;
let assets_path = Path::new(&manifest_dir).join("assets");
let templates_paths = vec![
Path::new(&manifest_dir).join("src/pages"),
Path::new(&manifest_dir).join("src/components"),
];
let livereload_layer =
get_livereload_layer(templates_paths).map_err(AppError::NotifyWatcher)?;
let router = get_router(assets_path).await.layer(livereload_layer);
let listener: TcpListener = get_tcp_listener().await.map_err(AppError::TCPListener)?;
let local_addr = listener.local_addr().map_err(AppError::TCPListener)?;
println!("Listening on: http://{}", local_addr);
// Run the server with the router
axum::serve(listener, router.into_make_service()).await?;
Ok(())
}

View File

@ -1,23 +0,0 @@
pub struct MenuItem {
pub id: String,
pub label: String,
pub href: String,
}
/// Get the menu items
/// This function is the central place to define the menu items
/// It can be used directly in templates, for example in the `navbar` component to render the menu
pub fn get_menu_items() -> Vec<MenuItem> {
vec![
MenuItem {
id: "home".to_string(),
label: "Accueil".to_string(),
href: "/".to_string(),
},
MenuItem {
id: "cps".to_string(),
label: "CPS".to_string(),
href: "/cps".to_string(),
},
]
}

View File

@ -1,43 +0,0 @@
{% extends "base.html" %}
{% import "navbar/navbar.html" as navbar -%}
{% block title %}Pharma Libre - CPS{% endblock %}
{% block body %}
{% call navbar::navbar(current="cps") %}
<div class="py-10">
<header id="page-header">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1
id="page-title"
class="text-3xl font-bold leading-tight tracking-tight text-gray-900"
>
CPS
</h1>
</div>
</header>
<main id="page-main">
<div
class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"
>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
>A</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div
class="border-2 border-dashed border-gray-300 rounded-lg dark:border-gray-600 h-32 md:h-64"
>B</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>C</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>D</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>E</div>
</div>
</div>
</main>
</div>
{% endblock %}

View File

@ -1,12 +0,0 @@
use askama_axum::Template;
use axum_htmx::HxRequest;
#[derive(Template)]
#[template(path = "cps.html")]
pub struct CpsTemplate {
hx_request: bool,
}
pub async fn cps(HxRequest(hx_request): HxRequest) -> CpsTemplate {
CpsTemplate { hx_request }
}

View File

@ -1,43 +0,0 @@
{% extends "base.html" %}
{% import "navbar/navbar.html" as navbar -%}
{% block title %}Pharma Libre - Accueil{% endblock %}
{% block body %}
{% call navbar::navbar(current="home") %}
<div class="py-10">
<header id="page-header">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1
id="page-title"
class="text-3xl font-bold leading-tight tracking-tight text-gray-900"
>
Accueil
</h1>
</div>
</header>
<main id="page-main">
<div
class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"
>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div
class="border-2 border-dashed border-gray-300 rounded-lg dark:border-gray-600 h-32 md:h-64"
>A</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>B</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>C</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>D</div>
</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
>E</div>
</div>
</main>
</div>
{% endblock %}

View File

@ -1,12 +0,0 @@
use askama_axum::Template;
use axum_htmx::HxRequest;
#[derive(Template)]
#[template(path = "home.html")]
pub struct GetHomeTemplate {
hx_request: bool,
}
pub async fn home(HxRequest(hx_request): HxRequest) -> GetHomeTemplate {
GetHomeTemplate { hx_request }
}

View File

@ -1,10 +0,0 @@
use axum::{routing, Router};
mod cps;
mod home;
pub fn get_routes() -> Router {
Router::new()
.route("/", routing::get(home::home))
.route("/cps", routing::get(cps::cps))
}

View File

@ -1,12 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.html',
'./css/**/*.css',
],
theme: {
extend: {},
},
plugins: [],
}

View File

@ -5,10 +5,25 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.89" anyhow = "1.0.89"
axum = "0.7.6" axum = { version = "0.7.6", features = ["macros"] }
listenfd = "1.0.1" listenfd = "1.0.1"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.6.1", features = ["cors"] }
sea-orm = { workspace = true, features = [
# Same `ASYNC_RUNTIME` and `DATABASE_DRIVER` as in the migration crate
"sqlx-sqlite",
"runtime-tokio-rustls",
"macros",
] }
serde.workspace = true
thiserror.workspace = true
entity = { path = "../../entity" }
migration = { path = "../../migration" }
utils = { path = "../utils" }
[dev-dependencies] [dev-dependencies]
cargo-watch = "8.5.2" cargo-watch = "8.5.2"
sea-orm-cli.workspace = true
systemfd = "0.4.3" systemfd = "0.4.3"

View File

@ -10,10 +10,19 @@ En développement, le mécanisme de hot-reload nécessite de disposer de `cargo-
cargo install cargo-watch systemfd cargo install cargo-watch systemfd
``` ```
## Configuration
> Astuce : lorsqu'on exécute directement la crate `backend` à des fins de développement, le système de configuration n'utilisera pas l'éventuel fichier `.env` situé à la racine du workspace Rust. Pour éviter de dupliquer le fichier `.env`, il est possible de créer un lien symbolique vers le fichier `.env` de la crate `backend` :
```bash
cd crates/backend
ln -s ../../.env .env
```
## Développement ## Développement
Pour lancer le serveur en mode développement, exécutez la commande suivante : Pour lancer le serveur en mode développement, exécutez la commande suivante :
```bash ```bash
systemfd --no-pid -s http::3030 -- cargo watch -x 'run --bin backend' systemfd --no-pid -s http::8080 -- cargo watch -w crates/backend -x 'run --bin backend'
``` ```

View File

@ -0,0 +1,48 @@
use axum::{extract::State, routing, Json};
use sea_orm::*;
use serde::Serialize;
use ::entity::{debug, debug::Entity as DebugEntity};
use crate::{AppError, AppState};
// DATABASE DEBUG CONTROLLERS
async fn get_debug_entries(db: &DatabaseConnection) -> Result<Vec<debug::Model>, DbErr> {
DebugEntity::find().all(db).await
}
async fn add_random_debug_entry(State(AppState { db_connection }): State<AppState>) {
let random_entry = debug::ActiveModel {
title: Set("Random title".to_string()),
text: Set("Random text".to_string()),
..Default::default()
};
random_entry.insert(&db_connection).await.unwrap();
}
// API HANDLER
#[derive(Serialize, Debug)]
struct DebugResponse {
db_ping_status: bool,
entries: Vec<debug::Model>,
}
#[axum::debug_handler]
async fn debug(
State(AppState { db_connection }): State<AppState>,
) -> Result<Json<DebugResponse>, AppError> {
let db_ping_status = db_connection.ping().await.is_ok();
let debug_entries = get_debug_entries(&db_connection).await?;
Ok(Json(DebugResponse {
db_ping_status,
entries: debug_entries,
}))
}
pub fn get_routes() -> axum::Router<crate::AppState> {
axum::Router::new()
.route("/", routing::get(debug))
.route("/add_random", routing::post(add_random_debug_entry))
}

View File

@ -0,0 +1,9 @@
use axum::Router;
use crate::AppState;
mod debug;
pub fn get_routes() -> Router<AppState> {
Router::new().nest("/debug", debug::get_routes())
}

11
crates/backend/src/db.rs Normal file
View File

@ -0,0 +1,11 @@
use migration::{Migrator, MigratorTrait};
use sea_orm::{Database, DatabaseConnection, DbErr};
use std::env;
pub async fn get_connection() -> Result<DatabaseConnection, DbErr> {
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let db_connection = Database::connect(database_url).await?;
Migrator::up(&db_connection, None).await?;
Ok(db_connection)
}

View File

@ -1,12 +1,47 @@
use anyhow::Error as AnyError; use anyhow::Error as AnyError;
use axum::http::{StatusCode, Uri}; use axum::http::{header, StatusCode, Uri};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use sea_orm::{DatabaseConnection, DbErr};
use thiserror::Error;
use tower_http::cors::{Any, CorsLayer};
pub fn get_router() -> Router { use ::utils::config::{load_config, ConfigError};
Router::new()
mod api;
mod db;
#[derive(Error, Debug)]
pub enum InitError {
#[error(transparent)]
ConfigError(#[from] ConfigError),
}
pub fn init() -> Result<(), InitError> {
load_config(None)?;
Ok(())
}
#[derive(Clone)]
pub struct AppState {
db_connection: DatabaseConnection,
}
pub async fn get_router() -> Result<Router, DbErr> {
let db_connection = db::get_connection().await?;
let state: AppState = AppState { db_connection };
let cors = CorsLayer::new()
.allow_methods(Any)
.allow_origin(Any)
.allow_headers([header::CONTENT_TYPE]);
Ok(Router::new()
.route("/", get(|| async { "Hello, world!" })) .route("/", get(|| async { "Hello, world!" }))
.merge(api::get_routes())
.fallback(fallback) .fallback(fallback)
.with_state(state)
.layer(cors))
} }
async fn fallback(uri: Uri) -> (StatusCode, String) { async fn fallback(uri: Uri) -> (StatusCode, String) {

View File

@ -1,24 +1,39 @@
use listenfd::ListenFd; use listenfd::ListenFd;
use thiserror::Error;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use backend::get_router; use backend::{get_router, init, InitError};
#[derive(Error, Debug)]
pub enum BackendError {
#[error("Error while setting up or serving the TCP listener")]
ServeTCPListener(#[from] std::io::Error),
#[error("Error while initialising the backend")]
InitError(#[from] InitError),
#[error("Error with the database connection")]
DatabaseConnection(#[from] sea_orm::DbErr),
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() -> Result<(), BackendError> {
let app = get_router(); init()?;
let app = get_router().await?;
let mut listenfd = ListenFd::from_env(); let mut listenfd = ListenFd::from_env();
let listener = match listenfd.take_tcp_listener(0)? {
let listener = match listenfd.take_tcp_listener(0).unwrap() {
// if we are given a tcp listener on listen fd 0, we use that one // if we are given a tcp listener on listen fd 0, we use that one
Some(listener) => { Some(listener) => {
listener.set_nonblocking(true).unwrap(); listener.set_nonblocking(true)?;
TcpListener::from_std(listener).unwrap() TcpListener::from_std(listener)?
} }
// otherwise fall back to local listening // otherwise fall back to local listening
None => TcpListener::bind("0.0.0.0:8080").await.unwrap(), None => TcpListener::bind("0.0.0.0:8080").await?,
}; };
println!("Listening on {}", listener.local_addr().unwrap()); let local_addr = listener.local_addr()?;
axum::serve(listener, app).await.unwrap(); println!("Listening on http://{}", local_addr);
axum::serve(listener, app).await?;
Ok(())
} }

View File

@ -13,8 +13,12 @@ crate-type = ["lib", "cdylib", "staticlib"]
tauri-build = { version = "2.0.0-rc", features = [] } tauri-build = { version = "2.0.0-rc", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.0.0-rc", features = [] } bytes = "1.6.1"
tauri-plugin-shell = "2.0.0-rc" http = "1.1.0"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tauri = { version = "2.0.0-rc", features = [] }
tauri-plugin-shell = "2.0.0-rc"
tower = "0.4.13"
thiserror.workspace = true

View File

@ -11,7 +11,8 @@ links = "ssvlux64,ssvosx,Ssvw64"
[dependencies] [dependencies]
libc = "0.2.159" libc = "0.2.159"
libloading = "0.8.5" libloading = "0.8.5"
thiserror = "1.0.64"
thiserror.workspace = true
[build-dependencies] [build-dependencies]
bindgen = "0.70.1" bindgen = "0.70.1"

View File

@ -0,0 +1,32 @@
# État d'avancement de l'implémentation des bindings FSV
| Module | Progression |
|-------------|------------------------------------|
| [SSV](#ssv) | ![](https://geps.dev/progress/5) |
| [SGD](#sgd) | ![](https://geps.dev/progress/0) |
| [SRT](#srt) | ![](https://geps.dev/progress/0) |
| [STS](#sts) | ![](https://geps.dev/progress/0) |
## SSV
| Fonctions implémentées |
|------------------------|
| SSV_InitLIB2 |
| SSV_LireConfig |
| SSV_LireCartePS |
## SGD
| Fonctions implémentées |
|------------------------|
## SRT
| Fonctions implémentées |
|------------------------|
## STS
| Fonctions implémentées |
|------------------------|

View File

@ -1,5 +1,18 @@
# FSV-sys, bindings Rust pour le package FSV SESAM-Vitale # FSV-sys, bindings Rust pour le package FSV SESAM-Vitale
## Librairies FSV
### Versions supportées
| Version FSV |
|-------------|
| 1.40.14 |
| 1.40.13 |
### État d'avancement de l'implémentation des bindings FSV
Les détails de l'avancement de l'implémentation des bindings FSV sont donnés dans le fichier [PROGRESS.md](PROGRESS.md)
## Utilisation ## Utilisation
### Pré-requis ### Pré-requis
@ -19,5 +32,5 @@
### Pré-requis ### Pré-requis
- Pour la génération des bindings lors de la pahse de `build` à l'aide de `bindgen`, il est nécessaire d'avoir installé `clang` ([documentation](https://rust-lang.github.io/rust-bindgen/requirements.html)). - Pour la génération des bindings lors de la phase de `build` à l'aide de `bindgen`, il est nécessaire d'avoir installé `clang` ([documentation](https://rust-lang.github.io/rust-bindgen/requirements.html)).

View File

@ -0,0 +1,7 @@
#ifndef WRAPPER_LINUX_H
#define WRAPPER_LINUX_H
#include "../../vendor/fsv/1.40.14.13/includes/SYS_DEF/linux/mc_sys_def.h"
#include "../../vendor/fsv/1.40.14.13/includes/SSV/pourFSV1.40.13/ssv.h"
#endif // WRAPPER_LINUX_H

View File

@ -0,0 +1,7 @@
#ifndef WRAPPER_MACOSX_H
#define WRAPPER_MACOSX_H
#include "../../vendor/fsv/1.40.14.13/includes/SYS_DEF/macosx/mc_sys_def.h"
#include "../../vendor/fsv/1.40.14.13/includes/SSV/pourFSV1.40.13/ssv.h"
#endif // WRAPPER_MACOSX_H

View File

@ -0,0 +1,7 @@
#ifndef WRAPPER_WIN_H
#define WRAPPER_WIN_H
#include "../../vendor/fsv/1.40.14.13/includes/SYS_DEF/win/mc_sys_def.h"
#include "../../vendor/fsv/1.40.14.13/includes/SSV/pourFSV1.40.13/ssv.h"
#endif // WRAPPER_WIN_H

View File

@ -0,0 +1,7 @@
#ifndef WRAPPER_LINUX_H
#define WRAPPER_LINUX_H
#include "../../vendor/fsv/1.40.14.13/includes/SYS_DEF/linux/mc_sys_def.h"
#include "../../vendor/fsv/1.40.14.13/includes/SSV/pourFSV1.40.14/ssv.h"
#endif // WRAPPER_LINUX_H

View File

@ -0,0 +1,7 @@
#ifndef WRAPPER_MACOSX_H
#define WRAPPER_MACOSX_H
#include "../../vendor/fsv/1.40.14.13/includes/SYS_DEF/macosx/mc_sys_def.h"
#include "../../vendor/fsv/1.40.14.13/includes/SSV/pourFSV1.40.14/ssv.h"
#endif // WRAPPER_MACOSX_H

View File

@ -0,0 +1,7 @@
#ifndef WRAPPER_WIN_H
#define WRAPPER_WIN_H
#include "../../vendor/fsv/1.40.14.13/includes/SYS_DEF/win/mc_sys_def.h"
#include "../../vendor/fsv/1.40.14.13/includes/SSV/pourFSV1.40.14/ssv.h"
#endif // WRAPPER_WIN_H

View File

@ -1,43 +1,11 @@
use std::{env, path::PathBuf}; use std::{env, path::PathBuf};
fn main() { // Supported versions of FSV
// // TODO: make the path dynamic static SUPPORTED_FSV_VERSIONS: [&str; 2] = ["1.40.14", "1.40.13"];
// println!("cargo:rustc-link-search=native=/home/florianbriand/TMP/SESAM-VITALE/FSV_1.40.1317_Linux/x86_64/Release/installeur/opt/santesocial/fsv/1.40.13/lib");
// Configure for various targets fn build_bindings(version: &str, target_code: &str) -> PathBuf {
let target_code;
// Use CARGO configuration env Variable, because !cfg(target_os) is not available in build.rs
// Source: https://kazlauskas.me/entries/writing-proper-buildrs-scripts
let target = env::var("TARGET").expect("TARGET not set"); let target = env::var("TARGET").expect("TARGET not set");
let target_os = env::var("CARGO_CFG_TARGET_OS"); let wrapper_path = format!("bindgen-wrappers/{}/wrapper.{}.h", version, target_code);
println!("Target: {:?}", target);
match target_os.as_ref().map(|x| &**x) {
Ok("linux") => {
println!("Building for Linux");
// lib_name = "ssvlux64";
target_code = "linux";
},
Ok("windows") => {
println!("Building for Windows");
// lib_name = "Ssvw64";
target_code = "win";
},
Ok("macos") => {
println!("Building for MacOS");
// lib_name = "ssvosx";
target_code = "macosx";
},
tos => panic!("Unsupported target_os {:?}", tos),
}
// Link the library
// println!("cargo:rustc-link-lib={}", lib_name);
// Build the bindings
let wrapper_path = format!("vendor/fsv/1.40.14.13/includes/wrapper.{}.h", target_code);
let bindings = bindgen::Builder::default() let bindings = bindgen::Builder::default()
// The input header we would like to generate // The input header we would like to generate
// bindings for. // bindings for.
@ -55,8 +23,35 @@ fn main() {
.expect("Unable to generate bindings"); .expect("Unable to generate bindings");
// Write the bindings to the $OUT_DIR/bindings.rs file. // Write the bindings to the $OUT_DIR/bindings.rs file.
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let out_file = format!("bindings_{}.rs", version);
let out_path = out_dir.join(out_file);
bindings bindings
.write_to_file(out_path.join("bindings.rs")) .write_to_file(&out_path)
.expect("Couldn't write bindings!"); .expect("Couldn't write bindings! ");
out_path
}
fn get_target_code() -> String {
// Use CARGO configuration env Variable, because !cfg(target_os) is not available in build.rs
// Source: https://kazlauskas.me/entries/writing-proper-buildrs-scripts
let target_os = env::var("CARGO_CFG_TARGET_OS");
match target_os.as_ref().map(|x| &**x) {
Ok("linux") => "linux", // lib_name = "ssvlux64";
Ok("windows") => "win", // lib_name = "Ssvw64";
Ok("macos") => "macosx", // lib_name = "ssvosx";
tos => panic!("Unsupported target_os {:?}", tos),
}
.to_string()
}
fn main() {
let target_code = get_target_code();
// Build the bindings for each supported version of FSV
let bindings_paths: Vec<PathBuf> = SUPPORTED_FSV_VERSIONS
.iter()
.map(|version| build_bindings(version, &target_code))
.collect();
println!("FSV bindings generated: {:#?}", bindings_paths);
} }

View File

@ -0,0 +1,12 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(unused)]
pub mod BINDINGS_V1_40_14 {
include!(concat!(env!("OUT_DIR"), "/bindings_1.40.14.rs"));
}
pub mod BINDINGS_V1_40_13 {
include!(concat!(env!("OUT_DIR"), "/bindings_1.40.13.rs"));
}

View File

@ -1,31 +1,23 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)] #![allow(non_snake_case)]
pub mod BINDINGS { use std::marker::PhantomData;
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
mod bindings;
use bindings::*;
#[derive(Debug, Clone)]
pub enum SupportedFsvVersion {
V1_40_14, // 1.40.14
V1_40_13, // 1.40.13
} }
// We need to keep the this use statement to get `ssv_function` macro working well impl SupportedFsvVersion {
use BINDINGS::*; fn as_str(&self) -> &'static str {
match self {
/// Macro to generate a function that implements a call to an external function in BINDINGS Self::V1_40_14 => "1.40.14",
macro_rules! ssv_function { Self::V1_40_13 => "1.40.13",
($binding:ty, $func_name:ident, {$($arg_name:ident: $arg_type:ty),*}) => { }
/// # Safety
/// This function is unsafe because it calls an external function through FFI.
/// The caller must ensure that the provided arguments are valid and that the
/// external function is safe to call.
pub unsafe fn $func_name(&self, $($arg_name: $arg_type),*) -> Result<u16, Error> {
let func_struct: libloading::Symbol<'_, $binding> =
unsafe { self.library.get(stringify!($binding).as_bytes())? };
let func = match *func_struct {
Some(func) => func,
None => return Err(Error::SymbolMissing(stringify!($binding))),
};
Ok(func($($arg_name),*))
} }
};
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -36,39 +28,81 @@ pub enum Error {
SymbolMissing(&'static str), SymbolMissing(&'static str),
} }
/// Macro to generate a function that implements a call to an external function in BINDINGS
macro_rules! ssv_function {
($binding:ty, $func_name:ident, {$($arg_name:ident: $arg_type:ty),*}) => {
/// # Safety
/// This function is unsafe because it calls an external function through FFI.
/// The caller must ensure that the provided arguments are valid and that the
/// external function is safe to call.
pub unsafe fn $func_name(&self, $($arg_name: $arg_type),*) -> Result<u16, Error> {
let symbol_name = match stringify!($binding)
.split(&[' ', ':'])
.last() {
Some(name) => name,
None => return Err(Error::SymbolMissing(stringify!($binding))),
};
let func_struct: libloading::Symbol<'_, $binding> =
unsafe { self.library.get(symbol_name.as_bytes())? };
let func = match *func_struct {
Some(func) => func,
None => return Err(Error::SymbolMissing(stringify!($binding))),
};
Ok(func($($arg_name),*))
}
};
}
/// `sealed::Sealed` trait is used to prevent external crates from implementing the LibVersion trait.
mod sealed { pub trait Sealed {}}
/// Wrapper around the SESAM-VITALE library /// Wrapper around the SESAM-VITALE library
/// This struct is responsible for loading the library and providing an interface to call its functions. /// This struct is responsible for loading the library and providing an interface to call its functions.
/// The library is loaded at creation and kept in memory until the struct is dropped. /// The library is loaded at creation and kept in memory until the struct is dropped.
#[derive(Debug)] pub trait SSVLibraryCommon {
pub struct SSVLibrary { fn new(path: &str) -> Result<Self, Error> where Self: Sized;
}
pub trait SSVLibraryVersion: sealed::Sealed {}
pub struct V1_40_13 {}
impl sealed::Sealed for V1_40_13 {}
impl SSVLibraryVersion for V1_40_13 {}
pub struct V1_40_14 {}
impl sealed::Sealed for V1_40_14 {}
impl SSVLibraryVersion for V1_40_14 {}
pub struct SSVLibrary<Version: SSVLibraryVersion> {
_version: PhantomData<Version>,
library: libloading::Library, library: libloading::Library,
} }
pub fn get_library_path() -> String { impl<Version: SSVLibraryVersion> SSVLibraryCommon for SSVLibrary<Version> {
// TODO : Use libloading::library_filename to get platform-specific filename ? fn new(path: &str) -> Result<Self, Error> {
"/opt/santesocial/fsv/1.40.13/lib/libssvlux64.so".to_string() let library = unsafe { libloading::Library::new(path)?};
Ok(Self {
_version: PhantomData,
library
})
}
} }
impl SSVLibrary { impl SSVLibrary<V1_40_14> {
pub fn new(library_path: &str) -> Result<Self, Error> {
let library = unsafe { libloading::Library::new(library_path)? };
Ok(SSVLibrary { library })
}
pub fn library(&self) -> &libloading::Library { pub fn library(&self) -> &libloading::Library {
&self.library &self.library
} }
ssv_function!(SSV_InitLIB2, ssv_init_lib2, { ssv_function!(BINDINGS_V1_40_14::SSV_InitLIB2, ssv_init_lib2, {
pcFichierSesam: *const i8 pcFichierSesam: *const i8
}); });
ssv_function!(SSV_LireConfig, ssv_lire_config, { ssv_function!(BINDINGS_V1_40_14::SSV_LireConfig, ssv_lire_config, {
pZDataOut: *mut *mut libc::c_void, pZDataOut: *mut *mut libc::c_void,
psTailleDataOut: *mut usize psTailleDataOut: *mut usize
}); });
ssv_function!(SSV_LireCartePS, ssv_lire_carte_ps, { ssv_function!(BINDINGS_V1_40_14::SSV_LireCartePS, ssv_lire_carte_ps, {
NomRessourcePS: *const i8, NomRessourcePS: *const i8,
NomRessourceLecteur: *const i8, NomRessourceLecteur: *const i8,
CodePorteurPS: *const i8, CodePorteurPS: *const i8,
@ -77,6 +111,51 @@ impl SSVLibrary {
}); });
} }
impl SSVLibrary<V1_40_13> {
ssv_function!(BINDINGS_V1_40_13::SSV_InitLIB2, ssv_init_lib2, {
pcFichierSesam: *const i8
});
ssv_function!(BINDINGS_V1_40_13::SSV_LireConfig, ssv_lire_config, {
pZDataOut: *mut *mut libc::c_void,
psTailleDataOut: *mut usize
});
ssv_function!(BINDINGS_V1_40_13::SSV_LireCartePS, ssv_lire_carte_ps, {
NomRessourcePS: *const i8,
NomRessourceLecteur: *const i8,
CodePorteurPS: *const i8,
pZDataOut: *mut *mut libc::c_void,
pTailleZone: *mut usize
});
}
pub fn get_library_path(version: &SupportedFsvVersion) -> String {
let root_path = get_library_root_path();
let library_name = get_library_name();
let version = version.as_str();
format!("{root_path}/{version}/lib/{library_name}")
}
pub fn sesam_ini_path(version: &SupportedFsvVersion) -> String {
let root_path = get_sesam_ini_root_path();
let version = version.as_str();
format!("{root_path}/{version}/conf/sesam.ini")
}
fn get_library_name() -> &'static str {
// TODO : Use libloading::library_filename to get platform-specific filename ?
"libssvlux64.so"
}
fn get_library_root_path() -> &'static str {
"/opt/santesocial/fsv"
}
fn get_sesam_ini_root_path() -> &'static str {
"/etc/opt/santesocial/fsv"
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::{ffi::CString, ptr}; use std::{ffi::CString, ptr};
@ -85,22 +164,22 @@ mod test {
#[test] #[test]
fn test_initlib2() { fn test_initlib2() {
let library_path = get_library_path(); let lib_path = &get_library_path(&SupportedFsvVersion::V1_40_13);
let ssv_library = SSVLibrary::new(&library_path).expect("SSVLibrary::new failed"); let ssv_library = SSVLibrary::<V1_40_13>::new(lib_path).expect("SSVLibrary::new failed");
let sesam_ini_str = CString::new("/etc/opt/santesocial/fsv/1.40.13/conf/sesam.ini") let sesam_ini_str =
.expect("CString::new failed"); CString::new(sesam_ini_path(&SupportedFsvVersion::V1_40_13)).expect("CString::new failed");
let result = unsafe { ssv_library.ssv_init_lib2(sesam_ini_str.as_ptr()) }.unwrap(); let result = unsafe { ssv_library.ssv_init_lib2(sesam_ini_str.as_ptr()) }.unwrap();
assert_eq!(result, 0); assert_eq!(result, 0);
} }
#[test] #[test]
fn test_lire_config_and_carte_ps() { fn test_lire_config_and_carte_ps() {
let library_path = get_library_path(); let lib_path = &get_library_path(&SupportedFsvVersion::V1_40_13);
let ssv_library = SSVLibrary::new(&library_path).expect("SSVLibrary::new failed"); let ssv_library = SSVLibrary::<V1_40_13>::new(lib_path).expect("SSVLibrary::new failed");
let sesam_ini_str = CString::new("/etc/opt/santesocial/fsv/1.40.13/conf/sesam.ini") let sesam_ini_str =
.expect("CString::new failed"); CString::new(sesam_ini_path(&SupportedFsvVersion::V1_40_13)).expect("CString::new failed");
let result = unsafe { ssv_library.ssv_init_lib2(sesam_ini_str.as_ptr()) }.unwrap(); let result = unsafe { ssv_library.ssv_init_lib2(sesam_ini_str.as_ptr()) }.unwrap();
assert_eq!(result, 0); assert_eq!(result, 0);
@ -108,11 +187,12 @@ mod test {
let mut size: libc::size_t = 0; let mut size: libc::size_t = 0;
let result = unsafe { ssv_library.ssv_lire_config(&mut buffer_ptr, &mut size) }.unwrap(); let result = unsafe { ssv_library.ssv_lire_config(&mut buffer_ptr, &mut size) }.unwrap();
assert_eq!(result, 0); assert_eq!(result, 0);
unsafe { libc::free(buffer_ptr) };
let nom_ressource_ps = CString::new("Gemalto PC Twin Reader (645D94C3) 00 00") let nom_ressource_ps =
.expect("CString::new failed"); CString::new("Gemalto PC Twin Reader (645D94C3) 00 00").expect("CString::new failed");
let nom_ressource_lecteur = CString::new("Gemalto PC Twin Reader (645D94C3) 00 00") let nom_ressource_lecteur =
.expect("CString::new failed"); CString::new("Gemalto PC Twin Reader (645D94C3) 00 00").expect("CString::new failed");
let code_porteur_ps = CString::new("1234").expect("CString::new failed"); let code_porteur_ps = CString::new("1234").expect("CString::new failed");
let mut buffer_ptr: *mut libc::c_void = ptr::null_mut(); let mut buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut size: libc::size_t = 0; let mut size: libc::size_t = 0;
@ -124,7 +204,9 @@ mod test {
&mut buffer_ptr, &mut buffer_ptr,
&mut size, &mut size,
) )
}.unwrap(); }
.unwrap();
assert_eq!(result, 0); assert_eq!(result, 0);
unsafe { libc::free(buffer_ptr) };
} }
} }

View File

@ -1,7 +0,0 @@
#ifndef WRAPPER_LINUX_H
#define WRAPPER_LINUX_H
#include "SYS_DEF/linux/mc_sys_def.h"
#include "SSV/pourFSV1.40.14/ssv.h"
#endif // WRAPPER_LINUX_H

View File

@ -1,7 +0,0 @@
#ifndef WRAPPER_MACOSX_H
#define WRAPPER_MACOSX_H
#include "SYS_DEF/macosx/mc_sys_def.h"
#include "SSV/pourFSV1.40.14/ssv.h"
#endif // WRAPPER_MACOSX_H

View File

@ -1,7 +0,0 @@
#ifndef WRAPPER_WIN_H
#define WRAPPER_WIN_H
#include "SYS_DEF/win/mc_sys_def.h"
#include "SSV/pourFSV1.40.14/ssv.h"
#endif // WRAPPER_WIN_H

13
crates/fsv/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "fsv"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.89"
libc = "0.2.159"
num_enum = { version = "0.7.3", features = ["complex-expressions"] }
thiserror = "1.0.64"
fsv-sys = { path = "../fsv-sys" }
utils = { path = "../utils" }

1
crates/fsv/src/lib.rs Normal file
View File

@ -0,0 +1 @@
mod ssv;

View File

@ -0,0 +1,183 @@
use num_enum::FromPrimitive;
use thiserror::Error;
#[derive(Error, Debug, Eq, PartialEq, FromPrimitive)]
#[repr(u16)]
/// Liste des codes d'erreur retournés par la librairie C SSV
/// Documentation: Manuel de programmation SSV - Annexe A (p. 215)
pub enum SSVErrorCodes {
#[error("La Carte du Professionnel de Santé est absente du lecteur.")]
CPSMissing = 0xF001,
#[error("La Carte du Professionnel de Santé bloquée après trois codes porteur erronés.")]
CPSBlocked = 0xF002,
#[error("Le code porteur présenté est erroné.")]
CPSPinWrong = 0xF003,
#[error("Carte du Professionnel de Santé non valide ou inexploitable par le Logiciel Lecteur. Vérifier la présence d'un Domaine d'Assurance Maladie (DAM).")]
CPSInvalid = 0xF004,
#[error("La Carte du Professionnel de Santé est retirée du lecteur.")]
CPSRemoved = 0xF005,
/// - Sécurisation d'une série de lots en cours.
/// - Pour les fonctions TLA (sauf Identifier TLA) : Cette erreur survient lorsque le simulateur TLA est en mode 1.50.
/// - Lire Date Lecteur, Mettre à jour Date Lecteur, Lire Droits Vitale : Cette erreur peut survenir lorsque le Logiciel Lecteur ne connaît pas la fonction sollicitée, c'est-à-dire si la version du Logiciel Lecteur est antérieure à 2.00.
/// - Décharger Données Bénéficiaires : cette erreur peut survenir pour signaler que le format des données issues du lecteur est incompatible avec cette version de SSV.
#[error("F022: Erreur commune à plusieurs fonctions.")]
F022 = 0xF022,
#[error("Message du lecteur incohérent. Débrancher et rebrancher le lecteur.")]
PCSCInconsistentMessage = 0xF0FF,
#[error("Le nom de lecteur fourni ne correspond à aucun lecteur reconnu.")]
PCSCReaderNotFound = 0xF101,
#[error("La fonction InitLIB2 n'est pas encore appelée ou la fonction TermLIB a déjà été appelée.")]
FunctionInitLib2NotCalled = 0xF600,
#[error("La bibliothèque SSV nest pas chargée en mémoire. Vérifier que la fonction InitLIB2 a bien été appelée.")]
LibraryNotLoaded = 0xF690, // Warning
#[error("Carte vitale en opposition.")]
VitaleOpposition = 0xF6A1,
#[error("Zone de mémoire non allouée en sortie.")]
MemoryNotAllocated = 0xF800,
#[error("Erreur d'allocation de la zone de mémoire en sortie.")]
MemoryAllocationError = 0xF801,
#[error("Un des paramètres obligatoires d'entrée est non alloué ou invalide.")]
InputParameterNotAllocatedOrInvalid = 0xF802,
#[error("Zone de mémoire spécifiée en entrée non valide. Vérifier que la zone allouée ne dépasse pas la taille maximale autorisée (MAXBLOC).")]
InputMemoryInvalid = 0xF803,
#[error("Le format de la zone de mémoire d'entrée ou le nombre de zones mémoire est incorrect.")]
InputMemoryFormatIncorrect = 0xF810,
#[error("Problème lors de linitialisation du protocole. Erreur du Ressource Manager PC/SC. Vérifiez le lecteur.")]
PCSCProtocolInitError = 0xFF01,
#[error("Time-out au niveau protocolaire ou transmission déjà en cours avec le lecteur. Vérifiez le lecteur et l'insertion de la carte.")]
PCSCProtocolTimeout = 0xFF02,
#[error("Taille insuffisante allouée en entrée dune fonction du Resource Manager.")]
PCSCProtocolInputMemoryTooSmall = 0xFF03,
#[error("Erreur de transmission du protocole. Vérifiez le lecteur et l'insertion de la carte.")]
PCSCProtocolTransmissionError = 0xFF04,
#[error("Lecteur absent ou indisponible.")]
PCSCReaderMissingOrUnavailable = 0xFF05,
#[error("Le nom du lecteur transmis est inconnu du Resource Manager PC/SC.")]
PCSCReaderUnknown = 0xFF06,
#[error("Erreur inconnue remontée par le Resource Manager PC/SC.")]
PCSCUnknownError = 0xFF07,
#[error("Erreur interne Resource Manager PC/SC.")]
PCSCInternalError = 0xFF08,
#[error("Ressource PC/SC déjà prise en exclusivité. Vérifiez qu'une autre application n'utilise pas le lecteur.")]
PCSCResourceAlreadyExclusive = 0xFF09,
#[error("Protocole incompatible avec la carte à puce. Vérifiez l'insertion de la carte et son état.")]
PCSCProtocolIncompatible = 0xFF0A,
#[error("Paramètre incorrect. Erreur interne à la librairie SSV.")]
PCSCIncorrectParameter = 0xFF0B,
#[error("Carte absente. Insérez une carte dans le lecteur.")]
PCSCCardMissing = 0xFF0C,
#[error("L'état de la carte a été modifié (RAZ ou mise hors tension). Vérifiez si la carte n'a pas été retirée ou si une autre application n'utilise pas la carte.")]
PCSCCardStateChanged = 0xFF0D,
#[error("Carte muette ou non supportée. Vérifiez l'insertion de la carte.")]
PCSCCardUnsupported = 0xFF0E,
#[error("Code porteur CPS non renseigné.")]
CPSPinMissing = 0xFF21,
#[error("Ressource PC/SC déjà prise en exclusivité. Vérifiez que le processus en cours n'utilise pas déjà le lecteur.")]
PCSCReaderAlreadyExclusiveForCurrentProcess = 0xFF24,
#[error("Plusieurs lecteurs ou cartes de même type identifiés lors de la détection automatique.")]
PCSCDuplicatedReadersOrCardsDetected = 0xFF29,
#[error("Problème de chargement de la librairie cryptographique ou erreur retournée par la librairie cryptographique.")]
CryptoLibraryError = 0xFF30,
#[error("Erreurs internes aux Services SESAM-Vitale. Vérifiez les traces.")]
#[num_enum(alternatives = [0xFFF1..=0xFFFF])]
SSVInternalError = 0xFFF0,
#[error("Le fichier `tablebin.smc` est inaccessible en lecture (inexistant ou pas de droits d'accès).")]
FileMissingTablebinMsc = 0xF610, // tablebin.smc
#[error("Le fichier `scripts.sms` est inaccessible en lecture (inexistant ou pas de droits d'accès).")]
FileMissingScriptsSms = 0xF611, // scripts.sms
#[error("Le fichier `tablebin.ssv` est inaccessible en lecture (inexistant ou pas de droits d'accès).")]
FileMissingTablebinSsv = 0xF612, // tablebin.ssv
#[error("Le fichier `script.ssv` est inaccessible en lecture (inexistant ou pas de droits d'accès).")]
FileMissingScriptSsv = 0xF613, // script.ssv
#[error("La version du fichier `tablebin.smc` est incompatible avec la bibliothèque des SSV.")]
FileVersionIncompatibleTablebinMsc = 0xF620, // tablebin.smc
#[error("La version du fichier `scripts.sms` est incompatible avec la bibliothèque des SSV.")]
FileVersionIncompatibleScriptsSms = 0xF621, // scripts.sms
#[error("La version du fichier `tablebin.ssv` est incompatible avec la bibliothèque des SSV.")]
FileVersionIncompatibleTablebinSsv = 0xF622, // tablebin.ssv
#[error("La version du fichier `script.ssv` est incompatible avec la bibliothèque des SSV.")]
FileVersionIncompatibleScriptSsv = 0xF623, // script.ssv
#[error("L'intégrité du fichier `tablebin.smc` est incorrecte.")]
FileIntegrityIncorrectTablebinMsc = 0xF630, // tablebin.smc
#[error("L'intégrité du fichier `scripts.sms` est incorrecte.")]
FileIntegrityIncorrectScriptsSms = 0xF631, // scripts.sms
#[error("L'intégrité du fichier `tablebin.ssv` est incorrecte.")]
FileIntegrityIncorrectTablebinSsv = 0xF632, // tablebin.ssv
#[error("L'intégrité du fichier `script.ssv` est incorrecte.")]
FileIntegrityIncorrectScriptSsv = 0xF633, // script.ssv
#[error("La structure interne du fichier `tablebin.smc` est invalide.")]
FileStructureInvalidTablebinMsc = 0xF640, // tablebin.smc
#[error("La structure interne du fichier `scripts.sms` est invalide.")]
FileStructureInvalidScriptsSms = 0xF641, // scripts.sms
#[error("La structure interne du fichier `tablebin.ssv` est invalide.")]
FileStructureInvalidTablebinSsv = 0xF642, // tablebin.ssv
#[error("La structure interne du fichier `script.ssv` est invalide.")]
FileStructureInvalidScriptSsv = 0xF643, // script.ssv
#[error("Le fichier `tablebin.smc` n'a pas pu être chargé en mémoire. Essayez de libérer de la mémoire.")]
FileLoadFailedTablebinMsc = 0xF650, // tablebin.smc
#[error("Le fichier `scripts.sms` n'a pas pu être chargé en mémoire. Essayez de libérer de la mémoire.")]
FileLoadFailedScriptsSms = 0xF651, // scripts.sms
#[error("Le fichier `tablebin.ssv` n'a pas pu être chargé en mémoire. Essayez de libérer de la mémoire.")]
FileLoadFailedTablebinSsv = 0xF652, // tablebin.ssv
#[error("Le fichier `script.ssv` n'a pas pu être chargé en mémoire. Essayez de libérer de la mémoire.")]
FileLoadFailedScriptSsv = 0xF653, // script.ssv
#[error("Le nom du fichier `tablebin.smc` est invalide.")]
FileNameInvalidTablebinMsc = 0xF660, // tablebin.smc
#[error("Le nom du fichier `scripts.sms` est invalide.")]
FileNameInvalidScriptsSms = 0xF661, // scripts.sms
#[error("Le nom du fichier `tablebin.ssv` est invalide.")]
FileNameInvalidTablebinSsv = 0xF662, // tablebin.ssv
#[error("Le nom du fichier `script.ssv` est invalide.")]
FileNameInvalidScriptSsv = 0xF663, // script.ssv
#[error("La fonction Initialiser Librairie est déjà appelée.")]
FunctionInitLib2AlreadyCalled = 0xF670, // Warning
#[error("Le fichier SESAM.INI est inaccessible en lecture (fichier ou droit daccès manquant) ou ne contient pas le chemin des tables binaires des SSV.")]
SesamIniMissingFileOrTablebinPath = 0xF680,
#[error("Le chemin du répertoire de travail est absent du fichier SESAM.INI.")]
SesamIniMissingWorkDir = 0xF6F1,
#[error("Les fichiers dextension adm ne sont pas accessibles en écriture.")]
AdmFilesNotWritable = 0xF6F2, // Warning
#[error("Aucune version de FSV du socle technique trouvé. Vérifier que la version du fichier script.sms est bonne.")]
NoFsvVersionFound = 0xF6F4,
#[error("Librairie SGD absente ou incomplète.")]
LibraryMissingOrIncompleteSGD = 0xF6F5,
#[error("Librairie SMC absente ou incomplète.")]
LibraryMissingOrIncompleteSMC = 0xF6F6,
#[error("Librairie SJS absente ou incomplète.")]
LibraryMissingOrIncompleteSJS = 0xF6F7,
#[error("Librairie SMS absente ou incomplète.")]
LibraryMissingOrIncompleteSMS = 0xF6F8,
#[error("Section MGC absente / clé RepertoireConfigTrace absente / fichier log4crc.xml non trouvé à lemplacement indiqué par la clé RepertoireConfigTrace du fichier SESAM.INI.")]
SesamIniTracingConfigMissing = 0xFF22, // Warning
#[error("Interface Full PC/SC : problème de chargement de la librairie cryptographique ou erreur retournée par la librairie cryptographique.")]
PCSCInterfaceCryptoLibraryError = 0xFF25,
#[error("Valorisation incorrecte des paramètres de gestion de l'accès aux ressources dans le SESAM.INI. Vérifier les valeurs des clés tempoexclusivite, repetitionexclusivite, tempoexclusivitePCSC, repetitionexclusivitePCSC")]
SesamIniResourceAccessParamsIncorrect = 0xFF2A,
#[num_enum(catch_all)]
#[error("Erreur inattendue de la librairie SSV (code d'erreur: {0}).")]
Unexpected(u16),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_code_ranges() {
let error_code = 0xFFF1;
let error = SSVErrorCodes::from(error_code);
assert_eq!(error, SSVErrorCodes::SSVInternalError);
let error_code = 0xFFF8;
let error = SSVErrorCodes::from(error_code);
assert_eq!(error, SSVErrorCodes::SSVInternalError);
}
#[test]
fn test_catch_all() {
let error_code = 0xFBFF; // Not a valid error code
let error = SSVErrorCodes::from(error_code);
assert_eq!(error, SSVErrorCodes::Unexpected(0xFBFF));
}
}

208
crates/fsv/src/ssv/mod.rs Normal file
View File

@ -0,0 +1,208 @@
use std::{ffi::CString, ptr};
use thiserror::Error;
use fsv_sys::{
get_library_path,
Error as FsvError,
SSVLibrary,
SSVLibraryCommon,
SupportedFsvVersion,
V1_40_13,
V1_40_14
};
mod errors_ssv;
use errors_ssv::SSVErrorCodes;
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
FSVSysLibrary(#[from] FsvError),
#[error(transparent)]
SSVError(#[from] SSVErrorCodes),
}
/// Enum to hold the different versions of the SSV library
enum SsvLibraryVersion {
V1_40_13(SSVLibrary<V1_40_13>),
V1_40_14(SSVLibrary<V1_40_14>),
}
/// Struct to hold the SSV library and access its functions
pub struct SSV {
library: SsvLibraryVersion,
}
impl SSV {
fn new(version: SupportedFsvVersion) -> Result<Self, Error> {
let library = match version {
SupportedFsvVersion::V1_40_13 => {
let lib_path = get_library_path(&version);
let library = SSVLibrary::<V1_40_13>::new(&lib_path)?;
SsvLibraryVersion::V1_40_13(library)
},
SupportedFsvVersion::V1_40_14 => {
let lib_path = get_library_path(&version);
let library = SSVLibrary::<V1_40_14>::new(&lib_path)?;
SsvLibraryVersion::V1_40_14(library)
},
};
Ok(Self {
library,
})
}
/// # Initialize the SSV library
/// Implement: SSV_InitLIB2
pub fn init_library(&self, sesam_ini_path: &str) -> Result<(), Error> {
let sesam_ini_path = CString::new(sesam_ini_path).expect("CString::new failed");
let result = match &self.library {
SsvLibraryVersion::V1_40_13(library) => {
unsafe { library.ssv_init_lib2(sesam_ini_path.as_ptr()) }?
},
SsvLibraryVersion::V1_40_14(library) => {
unsafe { library.ssv_init_lib2(sesam_ini_path.as_ptr()) }?
},
};
if result != 0 {
let error = SSVErrorCodes::from(result);
return Err(Error::SSVError(error));
}
Ok(())
}
/// # Read the CPS card
/// Implement: SSV_LireCartePS
pub fn read_professional_card(&self, pin_code: &str) -> Result<(), Error> {
let pcsc_reader_name = "Gemalto PC Twin Reader (645D94C3) 00 00";
let pin_code = CString::new(pin_code).expect("CString::new failed");
let pcsc_reader_name = CString::new(pcsc_reader_name).expect("CString::new failed");
let mut out_buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut out_buffer_size: libc::size_t = 0;
let result = match &self.library {
SsvLibraryVersion::V1_40_13(library) => {
unsafe { library.ssv_lire_carte_ps(
pcsc_reader_name.as_ptr(),
pcsc_reader_name.as_ptr(),
pin_code.as_ptr(),
&mut out_buffer_ptr,
&mut out_buffer_size)
}?
},
SsvLibraryVersion::V1_40_14(library) => {
unsafe { library.ssv_lire_carte_ps(
pcsc_reader_name.as_ptr(),
pcsc_reader_name.as_ptr(),
pin_code.as_ptr(),
&mut out_buffer_ptr,
&mut out_buffer_size)
}?
},
};
if result != 0 {
// Free memory
unsafe { libc::free(out_buffer_ptr) };
let error = SSVErrorCodes::from(result);
return Err(Error::SSVError(error));
}
// Print 10 bytes of the buffer
let buffer = unsafe { std::slice::from_raw_parts(out_buffer_ptr as *const u8, 10) };
println!("{:?}", buffer);
// Free memory
unsafe { libc::free(out_buffer_ptr) };
Ok(())
}
/// # Get the configuration of the SSV library
/// Implement: SSV_LireConfig
pub fn get_config(&self) -> Result<(), Error> {
let mut buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut size: libc::size_t = 0;
let result = match &self.library {
SsvLibraryVersion::V1_40_13(library) => {
unsafe { library.ssv_lire_config(&mut buffer_ptr, &mut size) }?
},
SsvLibraryVersion::V1_40_14(library) => {
unsafe { library.ssv_lire_config(&mut buffer_ptr, &mut size) }?
},
};
if result != 0 {
// Free memory
unsafe { libc::free(buffer_ptr) };
let error = SSVErrorCodes::from(result);
return Err(Error::SSVError(error));
}
// Print 10 bytes of the buffer
let buffer = unsafe { std::slice::from_raw_parts(buffer_ptr as *const u8, 10) };
println!("{:?}", buffer);
// Free memory
unsafe { libc::free(buffer_ptr) };
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::env;
use utils::config::load_config;
use anyhow::Result;
use super::*;
mod setup {
use super::*;
pub fn init() -> Result<SSV> {
load_config().unwrap();
let sesam_ini_path = env::var("SESAM_INI_PATH").expect("SESAM_INI_PATH must be set");
let lib = SSV::new(SupportedFsvVersion::V1_40_13)?;
lib.init_library(&sesam_ini_path)?;
Ok(lib)
}
}
#[test]
fn test_init_library() -> Result<()> {
setup::init()?;
Ok(())
}
#[test]
fn test_read_professional_card_good_pin() -> Result<()> {
let lib = setup::init()?;
let pin_code = "1234";
lib.read_professional_card(pin_code)?;
Ok(())
}
#[ignore]
#[test]
fn test_read_professional_card_bad_pin() -> Result<()> {
let lib = setup::init()?;
let pin_code = "0000";
// Should return an error
let err = lib.read_professional_card(pin_code).unwrap_err();
assert_eq!(err.to_string(), "Le code porteur présenté est erroné.");
match err {
Error::SSVError(err) => {
assert_eq!(err as SSVErrorCodes, SSVErrorCodes::CPSPinWrong);
},
_ => panic!("Error type is not SSVError"),
}
Ok(())
}
#[test]
fn test_get_config() -> Result<()> {
let lib = setup::init()?;
lib.get_config()?;
Ok(())
}
}

View File

@ -4,10 +4,11 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0" anyhow.workspace = true
libc = "0.2" libc = "0.2"
thiserror = "1.0" thiserror.workspace = true
utils = { path = "../utils" } utils = { path = "../utils" }
[build-dependencies] [build-dependencies]
dotenv = "0.15" dotenv.workspace = true

View File

@ -9,7 +9,7 @@ use thiserror::Error;
use crate::cps::lire_carte; use crate::cps::lire_carte;
use crate::libssv::{SSV_InitLIB2, SSV_LireConfig}; use crate::libssv::{SSV_InitLIB2, SSV_LireConfig};
use ::utils::config::load_config; use ::utils::config::{load_config, ConfigError};
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum SSVDemoError { pub enum SSVDemoError {
@ -18,7 +18,7 @@ pub enum SSVDemoError {
#[error(transparent)] #[error(transparent)]
SSVLibErrorCode(#[from] crate::libssv::LibSSVError), SSVLibErrorCode(#[from] crate::libssv::LibSSVError),
#[error(transparent)] #[error(transparent)]
Anyhow(#[from] anyhow::Error), Configuration(#[from] ConfigError),
} }
fn ssv_init_lib_2() -> Result<(), SSVDemoError> { fn ssv_init_lib_2() -> Result<(), SSVDemoError> {
@ -71,7 +71,7 @@ pub fn demo() -> Result<(), SSVDemoError> {
println!("------- Demo for the SSV library --------"); println!("------- Demo for the SSV library --------");
load_config()?; load_config(None)?;
ssv_init_lib_2()?; ssv_init_lib_2()?;

View File

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0" anyhow.workspace = true
directories = "5.0" directories = "5.0"
dotenv = "0.15" dotenv.workspace = true
thiserror.workspace = true

View File

@ -1,11 +1,23 @@
use std::{env, path::PathBuf}; use std::{env, path::PathBuf, sync::atomic::AtomicBool};
use anyhow::{bail, Context, Result};
use directories::ProjectDirs; use directories::ProjectDirs;
use dotenv::from_path; use dotenv::from_path;
use thiserror::Error;
const CONFIG_FILE_NAME: &str = ".env"; const CONFIG_FILE_NAME: &str = ".env";
static CONFIG_INITIALIZED: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("No config file {0} found in the following directories: {1:#?}")]
ConfigFileNotFound(String, Vec<PathBuf>),
#[error("Failed to load config file: {0}")]
LoadConfigError(#[from] dotenv::Error),
#[error("Environment variable error: {0}")]
EnvVarError(#[from] std::env::VarError),
}
pub fn get_config_dirs() -> Vec<PathBuf> { pub fn get_config_dirs() -> Vec<PathBuf> {
let mut config_dirs = vec![ let mut config_dirs = vec![
PathBuf::from(""), // Current directory PathBuf::from(""), // Current directory
@ -19,7 +31,7 @@ pub fn get_config_dirs() -> Vec<PathBuf> {
config_dirs config_dirs
} }
pub fn get_config_files() -> Result<Vec<PathBuf>> { pub fn get_config_files() -> Result<Vec<PathBuf>, ConfigError> {
let config_dirs = get_config_dirs(); let config_dirs = get_config_dirs();
let mut config_files = Vec::new(); let mut config_files = Vec::new();
for config_dir in config_dirs.iter() { for config_dir in config_dirs.iter() {
@ -29,14 +41,20 @@ pub fn get_config_files() -> Result<Vec<PathBuf>> {
} }
} }
if config_files.is_empty() { if config_files.is_empty() {
bail!( return Err(ConfigError::ConfigFileNotFound(
"No config file {CONFIG_FILE_NAME} found in the following directories: {config_dirs:#?}" CONFIG_FILE_NAME.to_string(),
); config_dirs,
));
} }
Ok(config_files) Ok(config_files)
} }
pub fn load_config() -> Result<()> { pub fn load_config(force: Option<bool>) -> Result<(), ConfigError> {
let force = force.unwrap_or(false);
if CONFIG_INITIALIZED.load(std::sync::atomic::Ordering::Relaxed) && force {
println!("DEBUG: Config already initialized, skipping");
return Ok(());
}
let config_files = get_config_files()?; let config_files = get_config_files()?;
// Load the first config file found // Load the first config file found
// TODO: add a verbose log to list all config files found // TODO: add a verbose log to list all config files found
@ -44,5 +62,7 @@ pub fn load_config() -> Result<()> {
"DEBUG: Config files found (1st loaded): {:#?}", "DEBUG: Config files found (1st loaded): {:#?}",
config_files config_files
); );
from_path(config_files[0].as_path()).context("Failed to load config file") from_path(config_files[0].as_path()).map_err(ConfigError::LoadConfigError)?;
CONFIG_INITIALIZED.store(true, std::sync::atomic::Ordering::Relaxed);
Ok(())
} }

15
entity/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "entity"
version = "0.1.0"
edition = "2021"
[lib]
name = "entity"
path = "src/lib.rs"
[dependencies]
sea-orm.workspace = true
serde.workspace = true
[dev-dependencies]
sea-orm-cli.workspace = true

View File

@ -0,0 +1,18 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "debug")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub title: String,
pub text: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,5 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
pub mod prelude;
pub mod debug;

View File

@ -0,0 +1,3 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
pub use super::debug::Entity as Debug;

2
entity/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
mod entities;
pub use entities::*;

View File

@ -1,5 +1,6 @@
<template> <template>
<div> <div>
<NuxtLoadingIndicator />
<NuxtRouteAnnouncer /> <NuxtRouteAnnouncer />
<NavBar /> <NavBar />
<NuxtPage /> <NuxtPage />

View File

@ -17,6 +17,6 @@
if (user.avatar) { if (user.avatar) {
return user.avatar; return user.avatar;
} }
return 'https://avatar.iran.liara.run/username?username=' + user.name; return 'https://i.pravatar.cc/150?u=' + user.name;
}; };
</script> </script>

View File

@ -29,15 +29,15 @@
const users: User[] = [ const users: User[] = [
{ id: 1, name: 'John Doe', avatar: 'https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp' }, { id: 1, name: 'John Doe', avatar: 'https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp' },
{ id: 2, name: 'Jane Doe', avatar: 'https://avatar.iran.liara.run/public' }, { id: 2, name: 'Jane Doe', avatar: 'https://i.pravatar.cc/150?u=JANEDOE728' },
{ id: 3, name: 'Michel Moulin', avatar: '' }, { id: 3, name: 'Michel Moulin' },
{ id: 4, name: 'Jean Paris', avatar: '' }, { id: 4, name: 'Jean Paris' },
{ id: 5, name: 'Marie Dupont', avatar: '' }, { id: 5, name: 'Marie Dupont' },
{ id: 6, name: 'Émilie Fournier', avatar: '' }, { id: 6, name: 'Émilie Fournier' },
{ id: 7, name: 'Pierre Lefevre', avatar: '' }, { id: 7, name: 'Pierre Lefevre' },
{ id: 8, name: 'Sophie Lemoine', avatar: '' }, { id: 8, name: 'Sophie Lemoine' },
{ id: 9, name: 'Lucie Simon', avatar: '' }, { id: 9, name: 'Lucie Simon' },
{ id: 10, name: 'Kevin Boucher', avatar: '' }, { id: 10, name: 'Kevin Boucher' },
]; ];
const loginModal = useTemplateRef('login_modal'); const loginModal = useTemplateRef('login_modal');

View File

@ -6,6 +6,7 @@
<nav class="navbar-center"> <nav class="navbar-center">
<NuxtLink to="/" class="btn btn-ghost">Accueil</NuxtLink> <NuxtLink to="/" class="btn btn-ghost">Accueil</NuxtLink>
<NuxtLink to="/CPS" class="btn btn-ghost">Carte CPS</NuxtLink> <NuxtLink to="/CPS" class="btn btn-ghost">Carte CPS</NuxtLink>
<NuxtLink to="/debug" class="btn btn-ghost">Debug</NuxtLink>
</nav> </nav>
<div class="navbar-end"> <div class="navbar-end">
<template v-if="!current_user"> <template v-if="!current_user">

67
frontend/pages/debug.vue Normal file
View File

@ -0,0 +1,67 @@
<template>
<div>
<h1 class="text-3xl mb-8">Debug</h1>
<div class="stats shadow mb-8">
<div class="stat">
<div class="stat-title">DB Ping Status</div>
<div class="stat-value">{{ data?.db_ping_status || "?" }}</div>
</div>
<div class="stat">
<div class="stat-title">Entries Count</div>
<div class="stat-value">{{ data?.entries.length || "?" }}</div>
<div class="stat-actions">
<button class="btn btn-sm" @click="addRandomEntry">Add entry</button>
</div>
</div>
<div class="stat">
<div class="stat-title">Network status</div>
<div class="stat-value">{{ status }}</div>
<div class="stat-description">{{ error }}</div>
</div>
</div>
<div>
<h2 class="text-2xl mb-4">Entries</h2>
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Text</th>
</tr>
</thead>
<tbody>
<tr v-for="entry in data?.entries" :key="entry.id">
<td>{{ entry.id }}</td>
<td>{{ entry.title }}</td>
<td>{{ entry.text }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
type Entry = {
id: number;
title: string;
text: string;
};
type DebugResponse = {
db_ping_status: string;
entries: Entry[];
};
// TODO : handle a default backend URL by building a custom `$fetch` and `useFetch` functions with a `baseURL` option : https://nuxt.com/docs/guide/recipes/custom-usefetch#custom-fetch
const { data, refresh, error, status } = await useFetch<DebugResponse>('http://127.0.0.1:8080/debug');
async function addRandomEntry() {
await $fetch('http://127.0.0.1:8080/debug/add_random', {
method: 'POST',
});
refresh();
}
</script>

24
migration/Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
[dev-dependencies]
sea-orm-cli.workspace = true
[dependencies.sea-orm-migration]
version = "1.0.0"
features = [
# `ASYNC_RUNTIME` and `DATABASE_DRIVER` are required to run migration using the cli
# They must be the same as the features in the `sea-orm` dependency in the `app` crate
"sqlx-sqlite", # `DATABASE_DRIVER` feature
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
]

41
migration/README.md Normal file
View File

@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

12
migration/src/lib.rs Normal file
View File

@ -0,0 +1,12 @@
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_debug_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_debug_table::Migration)]
}
}

View File

@ -0,0 +1,35 @@
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Debug::Table)
.if_not_exists()
.col(pk_auto(Debug::Id))
.col(string(Debug::Title))
.col(string(Debug::Text))
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Debug::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Debug {
Table,
Id,
Title,
Text,
}

6
migration/src/main.rs Normal file
View File

@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(migration::Migrator).await;
}