12 Commits

64 changed files with 2737 additions and 2504 deletions

View File

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

View File

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

3
.gitignore vendored
View File

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

2340
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,16 +4,6 @@ members = [
"crates/backend",
"crates/desktop",
"crates/sesam-vitale",
"crates/services-sesam-vitale-sys",
"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

@ -43,24 +43,6 @@ La CLI Tauri est nécessaire au lancement du client `desktop`. Elle peut être i
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`.
#### SESAM-Vitale
La crate `sesam-vitale` nécessite la présence des librairies dynamiques 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 `sesam-vitale`.
@ -75,7 +57,7 @@ Pour lancer l'application en mode développement, il est nécessaire d'exécuter
```bash
# Lancement du serveur backend
systemfd --no-pid -s http::8080 -- cargo watch -w crates/backend -x 'run --bin backend'
systemfd --no-pid -s http::3030 -- cargo watch -x 'run --bin backend'
```
```bash
@ -83,13 +65,9 @@ systemfd --no-pid -s http::8080 -- cargo watch -w crates/backend -x 'run --bin b
# - frontend (serveur web, accessible via navigateur)
bun run --cwd frontend/ dev
# - desktop (client desktop, basé sur Tauri)
cargo tauri dev --no-watch
cargo tauri dev
```
> 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
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 :
@ -97,25 +75,3 @@ Pour packager le client `desktop`, il est nécessaire de faire appel à la CLI T
```bash
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
```

4
crates/app/.gitignore vendored Normal file
View File

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

21
crates/app/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[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"

35
crates/app/README.md Normal file
View File

@ -0,0 +1,35 @@
## 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_.

6
crates/app/askama.toml Normal file
View File

@ -0,0 +1,6 @@
[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

3
crates/app/css/input.css Normal file
View File

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

View File

@ -0,0 +1,23 @@
{% 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

@ -0,0 +1,18 @@
{% 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

@ -0,0 +1,50 @@
{% 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

@ -0,0 +1,22 @@
<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

@ -0,0 +1,4 @@
<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

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

21
crates/app/src/lib.rs Normal file
View File

@ -0,0 +1,21 @@
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)
}

84
crates/app/src/main.rs Normal file
View File

@ -0,0 +1,84 @@
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(())
}

23
crates/app/src/menu.rs Normal file
View File

@ -0,0 +1,23 @@
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

@ -0,0 +1,43 @@
{% 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

@ -0,0 +1,12 @@
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

@ -0,0 +1,43 @@
{% 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

@ -0,0 +1,12 @@
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

@ -0,0 +1,10 @@
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

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

View File

@ -5,25 +5,10 @@ edition = "2021"
[dependencies]
anyhow = "1.0.89"
axum = { version = "0.7.6", features = ["macros"] }
axum = "0.7.6"
listenfd = "1.0.1"
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]
cargo-watch = "8.5.2"
sea-orm-cli.workspace = true
systemfd = "0.4.3"

View File

@ -10,19 +10,10 @@ En développement, le mécanisme de hot-reload nécessite de disposer de `cargo-
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
Pour lancer le serveur en mode développement, exécutez la commande suivante :
```bash
systemfd --no-pid -s http::8080 -- cargo watch -w crates/backend -x 'run --bin backend'
systemfd --no-pid -s http::3030 -- cargo watch -x 'run --bin backend'
```

View File

@ -1,48 +0,0 @@
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

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

View File

@ -1,11 +0,0 @@
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,47 +1,12 @@
use anyhow::Error as AnyError;
use axum::http::{header, StatusCode, Uri};
use axum::http::{StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use axum::{routing::get, Router};
use sea_orm::{DatabaseConnection, DbErr};
use thiserror::Error;
use tower_http::cors::{Any, CorsLayer};
use ::utils::config::{load_config, ConfigError};
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()
pub fn get_router() -> Router {
Router::new()
.route("/", get(|| async { "Hello, world!" }))
.merge(api::get_routes())
.fallback(fallback)
.with_state(state)
.layer(cors))
}
async fn fallback(uri: Uri) -> (StatusCode, String) {

View File

@ -1,39 +1,24 @@
use listenfd::ListenFd;
use thiserror::Error;
use tokio::net::TcpListener;
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),
}
use backend::get_router;
#[tokio::main]
async fn main() -> Result<(), BackendError> {
init()?;
let app = get_router().await?;
async fn main() {
let app = get_router();
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
Some(listener) => {
listener.set_nonblocking(true)?;
TcpListener::from_std(listener)?
listener.set_nonblocking(true).unwrap();
TcpListener::from_std(listener).unwrap()
}
// otherwise fall back to local listening
None => TcpListener::bind("0.0.0.0:8080").await?,
None => TcpListener::bind("0.0.0.0:8080").await.unwrap(),
};
let local_addr = listener.local_addr()?;
println!("Listening on http://{}", local_addr);
axum::serve(listener, app).await?;
Ok(())
println!("Listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}

View File

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

View File

@ -0,0 +1,12 @@
[package]
name = "services-sesam-vitale-sys"
version = "0.1.0"
edition = "2021"
#links= "ssvlux64"
[dependencies]
bitvec = "1.0.1"
deku = "0.17.0"
libc = "0.2.155"
num_enum = { version = "0.7.3", features = ["complex-expressions"] }
thiserror = "1.0.63"

View File

@ -0,0 +1,104 @@
use deku::{deku_derive, DekuContainerRead, DekuError, DekuReader};
use std::{ffi::CString, fmt, path::Path, ptr};
use thiserror::Error;
use crate::{
bindings::{SSV_InitLIB2, SSV_LireConfig, SSV_TermLIB},
types::{common::read_from_buffer, configuration::Configuration},
};
use num_enum::FromPrimitive;
#[derive(Error, Debug)]
pub struct SesamVitaleError {
code: u16,
}
#[derive(Debug, Eq, PartialEq, FromPrimitive)]
#[repr(u16)]
enum SSVIntError {
CPSNotInserted = 61441,
#[num_enum(catch_all)]
NotImplemented(u16),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sesam_vitale_error() {
let int_error = SSVIntError::from(61441);
assert_eq!(int_error, SSVIntError::CPSNotInserted);
let int_error = SSVIntError::from(123);
assert_eq!(int_error, SSVIntError::NotImplemented(123));
println!("{:?}", int_error);
}
}
#[derive(Error, Debug)]
enum SSVError {
#[error("Erreur standard de la librairie SSV")]
SSVStandard,
// #[error("Erreur de parsing")]
// Parsing(#[from] ParsingError),
#[error("Erreur inattendue de la librairie SSV (TMP)")]
SSVUnknownTmp,
}
impl fmt::Display for SesamVitaleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Got error code {} from SSV_LireConfig", self.code)
}
}
pub fn init_library(sesam_ini_path: &Path) -> Result<(), SesamVitaleError> {
// TODO: better error handling
let path_str = sesam_ini_path.to_str().unwrap();
let path_ptr = CString::new(path_str).expect("failed to create cstring");
let exit_code: u16 = unsafe { SSV_InitLIB2(path_ptr.as_ptr()) };
if exit_code != 0 {
let error = SesamVitaleError { code: exit_code };
return Err(error);
};
Ok(())
}
pub fn close_library() -> Result<(), SesamVitaleError> {
let exit_code: u16 = unsafe { SSV_TermLIB() };
if exit_code != 0 {
let error = SesamVitaleError { code: exit_code };
return Err(error);
};
Ok(())
}
pub fn read_config() -> Result<Configuration, SesamVitaleError> {
let mut buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut size: libc::size_t = 0;
let buffer_ptr_ptr: *mut *mut libc::c_void = &mut buffer_ptr;
let size_ptr: *mut libc::size_t = &mut size;
// Need to add proper error handling -> return a result with error code pointing to an error
// enum
let exit_code: u16 = unsafe { SSV_LireConfig(buffer_ptr_ptr, size_ptr) };
if exit_code != 0 {
let error = SesamVitaleError { code: exit_code };
return Err(error);
};
let buffer: &[u8] = unsafe { std::slice::from_raw_parts(buffer_ptr as *const u8, size) };
// TODO: Improve error handling
let configuration: Configuration = read_from_buffer(buffer).unwrap();
// TODO: Call library function for memory delocating
unsafe { libc::free(buffer_ptr) };
Ok(configuration)
}

View File

@ -0,0 +1,288 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
// Generated using bindgen
extern "C" {
// Fonctions de gestion des données
pub fn SSV_LireCartePS(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_LireDroitsVitale(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
DateConsultation: *const ::std::os::raw::c_char,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_FormaterFactures(
cFactureACreer: ::std::os::raw::c_char,
cModeSecur: ::std::os::raw::c_char,
cTypeFlux: ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
TailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_ChiffrerFacture(
pZDataIn: *mut ::std::os::raw::c_void,
TailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_SignerFactureVitale(
pcNomRessourceVitale: *const ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_CalculerHashFactureAssure(
pcNumSerie: *const ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_AjouterSignatureAssureDansFacture(
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_SignerFactureCPS(
pcNomRessourcePS: *const ::std::os::raw::c_char,
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcCodePorteurPS: *const ::std::os::raw::c_char,
cNologSituation: ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_FormaterLot(
NBZDataIn: ::std::os::raw::c_short,
TZDataIn: *mut *mut ::std::os::raw::c_void,
TTailleZoneIn: *mut usize,
pNbZDataOut: *mut ::std::os::raw::c_short,
TZDataOut: *mut *mut ::std::os::raw::c_void,
TTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_SignerLotCPS(
pcNomRessourcePS: *const ::std::os::raw::c_char,
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcCodePorteurPS: *const ::std::os::raw::c_char,
cNologSituation: ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_FormaterFichier(
pZDataIn: *mut ::std::os::raw::c_void,
TailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_TraduireARL(
NbZDonneesEntree: ::std::os::raw::c_short,
TZDataIn: *mut *mut ::std::os::raw::c_void,
TTailleZoneIn: *mut usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_LireNumSerieCarteVitale(
pcNomRessource: *mut ::std::os::raw::c_char,
numeroSerie: *mut ::std::os::raw::c_uchar,
) -> ::std::os::raw::c_ushort;
pub fn SSV_CalculerHashFacturePS(
pcNumSerieCPS: *const ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
usTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pusTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_AjouterSignaturePSFacture(
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_DechargerFacturesPdT(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
pcNumFact: *const ::std::os::raw::c_char,
sNbZDataIn: ::std::os::raw::c_short,
pvTZDataIn: *mut *mut ::std::os::raw::c_void,
psTTailleDataIn: *mut usize,
pNbZDataOut: *mut ::std::os::raw::c_short,
TZDataOut: *mut *mut ::std::os::raw::c_void,
TTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_TraduireFSE(
pZDataIn: *mut ::std::os::raw::c_void,
TailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
// Fonctions TLA
// TLA (Terminal Lecteur Applicatif) -> lecteur autre que PC-SC, on ne prend pas en compte cela
pub fn SSV_IdentifierTLA(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
NumVersionCDC: *const ::std::os::raw::c_char,
pZDataOut: *mut *mut ::std::os::raw::c_void,
tailleDataOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_ChargerDonneesTLA(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
sNbZDataIn: ::std::os::raw::c_short,
pvTZDataIn: *mut *mut ::std::os::raw::c_void,
psTTailleDataIn: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_ChargerFacturesPdT(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcNumFacturation: *const ::std::os::raw::c_char,
sNbZDataIn: ::std::os::raw::c_short,
pvTZDataIn: *mut *mut ::std::os::raw::c_void,
psTTailleDataIn: *mut usize,
pNbZDataOut: *mut ::std::os::raw::c_short,
TZDataOut: *mut *mut ::std::os::raw::c_void,
TTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_DechargerFSETLA(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
pcNumFact: *const ::std::os::raw::c_char,
pNbZDataOut: *mut ::std::os::raw::c_short,
TZDataOut: *mut *mut ::std::os::raw::c_void,
TTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_DechargerFSETLANC(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
pcNumFact: *const ::std::os::raw::c_char,
pNbZDataOut: *mut ::std::os::raw::c_short,
TZDataOut: *mut *mut ::std::os::raw::c_void,
TTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_DechargerBeneficiaires(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
cNumFacturation: *const ::std::os::raw::c_char,
sNbZDataOut: *mut ::std::os::raw::c_short,
pTZDataOut: *mut *mut ::std::os::raw::c_void,
sTTailleDataOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_EffacerTLA(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
cNumFacturation: *const ::std::os::raw::c_char,
cTypeDonnee: *const ::std::os::raw::c_char,
) -> ::std::os::raw::c_ushort;
pub fn SSV_SecuriserFacture(
pcNomRessourcePS: *const ::std::os::raw::c_char,
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcCodePorteurPS: *const ::std::os::raw::c_char,
cNologSituation: ::std::os::raw::c_char,
pcNumFact: *const ::std::os::raw::c_char,
pvDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pvDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleDataOut: *mut usize,
) -> ::std::os::raw::c_ushort;
// Fonctions de gestion de configuration (GALSS)
pub fn SSV_LireConfig(
pZDataOut: *mut *mut ::std::os::raw::c_void,
psTailleDataOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_LireDateLecteur(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcDateHeure: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_ushort;
pub fn SSV_MajDateLecteur(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcDateHeure: *const ::std::os::raw::c_char,
) -> ::std::os::raw::c_ushort;
pub fn SSV_ChargerAppli(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
sNbZDataIn: ::std::os::raw::c_short,
pvTZDataIn: *mut *mut ::std::os::raw::c_void,
psTTailleDataIn: *mut usize,
) -> ::std::os::raw::c_ushort;
// Fonctions techniques
// La fonction Initialiser Librairie a pour objet de charger et dinitialiser dans la mémoire du système :
// - dans le cas où le GALSS est installé sur le poste :
// - la bibliothèque du Gestionnaire dAccès au Lecteur Santé Social (GALSS),
// - qui charge la bibliothèque du Protocole Santé Social (PSS),
// - la configuration du poste de travail à laide du fichier galssinf,
// - les variables globales communes aux différents Services SESAM-Vitale,
// - les fichiers de tables et scripts des répertoires par défaut.
// Cette fonction accède au référentiel électronique en utilisant le chemin complet indiqué dans le fichier sesam.ini.
pub fn SSV_InitLIB2(pcFichierSesam: *const ::std::os::raw::c_char) -> ::std::os::raw::c_ushort;
// La fonction Terminer a pour objet de décharger de la mémoire du système les éléments
// chargés par la fonction Initialiser Librairie, qui ne sont plus utiles.
pub fn SSV_TermLIB() -> ::std::os::raw::c_ushort;
/// Fonctions de Tracage
//La fonction Allouer Zone Mémoire a un rôle purement technique : elle permet dallouer, autrement dit de réserver une zone ou partie de la mémoire du poste de travail pour y écrire les données à passer en entrée dun Service SESAM-Vitale.
// Cette fonction doit être utilisée pour allouer toutes les zones de mémoire requises en entrée des Services SESAM-Vitale de manière à permettre un diagnostic fiable par le « mode trace » en cas de dysfonctionnement. En effet, son mode dexécution est susceptible de fournir des informations utiles au « mode trace » lorsquil est activé.
pub fn SSV_AllouerZoneMem(
pZDataIn: *mut *mut ::std::os::raw::c_void,
taille: usize,
) -> ::std::os::raw::c_ushort;
// La fonction Libérer Zone Mémoire a un rôle purement technique : elle permet de libérer une zone de mémoire du poste de travail précédemment allouée après exploitation des données quelle contient.
// Cette fonction doit être utilisée pour libérer toutes les zones de mémoire :
// - celles qui ont été allouées par le progiciel de santé pour fournir les données nécessaires à lentrée des Services SESAM-Vitale, avant leur appel, celles qui ont été allouées par les Services SESAM-Vitale pour fournir en sortie les données utiles au progiciel de santé qui a fait appel à ces services,
// - de façon à permettre un diagnostic fiable par le mode trace en cas de dysfonctionnement
//En effet, son exécution est susceptible de fournir des informations utiles au « mode trace » lorsquil est activé.
pub fn SSV_LibererZoneMem(pZone: *mut ::std::os::raw::c_void);
// La fonction Initialiser Trace a pour objet de permettre lactivation du « mode trace ».
// Ce mode de fonctionnement est prévu pour permettre à lassistance technique du GIE
// SESAM-Vitale danalyser les problèmes de mise en œuvre des Services SESAM-Vitale,
// notamment lorsque une fonction retourne un code derreur de valeur hexadécimale supérieure à FF00.
pub fn SSV_InitTrace(
pathConf: *mut ::std::os::raw::c_char,
ModeOuvertureFicherLog: *mut ::std::os::raw::c_char,
ModuleLog: ::std::os::raw::c_ushort,
NiveauLog: ::std::os::raw::c_uchar,
) -> ::std::os::raw::c_ushort;
}

View File

@ -0,0 +1,6 @@
pub mod api;
mod bindings;
pub mod types;
#[cfg(test)]
mod tests {}

View File

@ -0,0 +1,144 @@
use crate::types::configuration::{
ConfigurationHeader, PCSCReader, ReaderConfiguration, SESAMVitaleComponent,
};
use std::{error::Error, str::FromStr};
use bitvec::index::BitIdx;
use deku::{
bitvec::{BitStore, Msb0},
ctx::ByteSize,
deku_derive,
reader::{Reader, ReaderRet},
DekuContainerRead, DekuError, DekuReader,
};
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct NumericString(#[deku(map = "convert_from_data_field")] String);
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AlphaNumericString(#[deku(map = "convert_from_data_field")] String);
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct BinaryData(#[deku(map = "extract_from_data_field")] Vec<u8>);
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, Copy, PartialEq)]
#[deku(endian = "big")]
pub(crate) struct GroupId(u16);
trait MapToDekuParseError<T> {
fn map_to_deku_parse_error(self) -> Result<T, DekuError>;
}
impl<T, E: Error> MapToDekuParseError<T> for Result<T, E> {
fn map_to_deku_parse_error(self) -> Result<T, DekuError> {
self.map_err(|e| DekuError::Parse(e.to_string().into()))
}
}
fn read_size<R: std::io::Read>(reader: &mut Reader<R>) -> Result<ByteSize, DekuError> {
let first_byte: u8 = u8::from_reader_with_ctx(reader, ())?;
let is_length_expanded = first_byte.get_bit::<Msb0>(BitIdx::new(0).map_to_deku_parse_error()?);
match is_length_expanded {
true => {
let size_of_data_size: ByteSize = ByteSize((first_byte & 0b0111_1111) as usize);
if size_of_data_size.0 > 4 {
return Err(DekuError::Parse("Size of the length encoding is > 4, this is not normal. Probable parsing error".to_string().into()));
};
// maximum size of the buffer is 4, we use the offset to read values less than 4 bytes
let buffer: &mut [u8; 4] = &mut [0; 4];
let write_offset = 4 - size_of_data_size.0;
match reader.read_bytes(size_of_data_size.0, &mut buffer[write_offset..])? {
ReaderRet::Bits(_bit_vec) => Err(DekuError::Parse("Got bits when trying to read bytes -> reader is unaligned, this is not normal.".to_string().into())),
ReaderRet::Bytes => Ok(ByteSize(u32::from_be_bytes(*buffer) as usize)),
}
}
false => Ok(ByteSize(first_byte as usize)),
}
}
// Using this as the map function asks deku to parse a datafield
// We then use the datafield and convert it to the corresponding value
pub(super) fn convert_from_data_field<T>(data_field: DataField) -> Result<T, DekuError>
where
T: FromStr,
T::Err: Error,
{
let text = String::from_utf8(data_field.data).map_to_deku_parse_error()?;
T::from_str(&text).map_to_deku_parse_error()
}
pub(crate) fn extract_from_data_field(data_field: DataField) -> Result<Vec<u8>, DekuError> {
Ok(data_field.data)
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub(crate) struct DataField {
#[deku(reader = "read_size(deku::reader)")]
pub(crate) data_size: ByteSize,
#[deku(bytes_read = "data_size.0")]
pub(crate) data: Vec<u8>,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub(crate) struct BlockHeader {
pub(crate) group_id: GroupId,
#[deku(reader = "read_size(deku::reader)")]
pub(crate) data_size: ByteSize,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub(crate) struct DataBlock {
pub(crate) header: BlockHeader,
#[deku(ctx = "header.group_id")]
pub(crate) inner: DataGroup,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "group_id: GroupId", id = "group_id.0")]
pub enum DataGroup {
#[deku(id = 60)]
ConfigurationHeader(ConfigurationHeader),
#[deku(id = 61)]
ReaderConfiguration(ReaderConfiguration),
#[deku(id = 64)]
SESAMVitaleComponent(SESAMVitaleComponent),
#[deku(id = 67)]
PCSCReader(PCSCReader),
}
pub(crate) fn read_from_buffer<T>(buffer: &[u8]) -> Result<T, T::Error>
where
T: TryFrom<Vec<DataBlock>>,
{
let mut data_blocks: Vec<DataBlock> = Vec::new();
let mut offset = 0;
let mut remaining_buffer = buffer;
while !remaining_buffer.is_empty() {
// TODO: properly handle errors
let (rest, data_block) = DataBlock::from_bytes((remaining_buffer, offset)).unwrap();
data_blocks.push(data_block);
(remaining_buffer, offset) = rest;
}
T::try_from(data_blocks)
}

View File

@ -0,0 +1,137 @@
use crate::types::common::DataBlock;
use std::{error::Error, fmt, vec::Vec};
use crate::types::common::convert_from_data_field;
use deku::{deku_derive, DekuReader};
use super::common::{AlphaNumericString, DataGroup};
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SSVVersionNumber(#[deku(map = "convert_from_data_field")] u16);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct GALSSVersionNumber(#[deku(map = "convert_from_data_field")] u16);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct PSSVersionNumber(#[deku(map = "convert_from_data_field")] u16);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct ConfigurationHeader {
pub ssv_version: SSVVersionNumber,
pub galss_version: GALSSVersionNumber,
pub pss_version: PSSVersionNumber,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct PCSCReaderName(AlphaNumericString);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct CardType(#[deku(map = "convert_from_data_field")] u8);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct PCSCReader {
pub name: PCSCReaderName,
pub card_type: CardType,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SESAMVitaleComponentID(#[deku(map = "convert_from_data_field")] u16);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SESAMVitaleComponentDescription(AlphaNumericString);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SESAMVitaleComponentVersion(AlphaNumericString);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SESAMVitaleComponent {
pub id: SESAMVitaleComponentID,
pub description: SESAMVitaleComponentDescription,
pub version: SESAMVitaleComponentVersion,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct ReaderConfiguration {}
#[derive(Debug)]
pub enum ConfigurationError {
MultipleConfigurationHeaders,
MissingConfigurationHeader,
}
impl fmt::Display for ConfigurationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigurationError::MultipleConfigurationHeaders => {
write!(f, "Multiple ConfigurationHeader blocks found")
}
ConfigurationError::MissingConfigurationHeader => {
write!(f, "Missing ConfigurationHeader block")
}
}
}
}
impl Error for ConfigurationError {}
#[derive(Debug)]
pub struct Configuration {
pub configuration_header: ConfigurationHeader,
pub reader_configurations: Vec<ReaderConfiguration>,
pub sesam_vitale_components: Vec<SESAMVitaleComponent>,
pub pcsc_readers: Vec<PCSCReader>,
}
impl TryFrom<Vec<DataBlock>> for Configuration {
type Error = ConfigurationError;
fn try_from(data_blocks: Vec<DataBlock>) -> Result<Self, Self::Error> {
let mut configuration_header: Option<ConfigurationHeader> = None;
let mut reader_configurations: Vec<ReaderConfiguration> = Vec::new();
let mut sesam_vitale_components: Vec<SESAMVitaleComponent> = Vec::new();
let mut pcsc_readers: Vec<PCSCReader> = Vec::new();
for block in data_blocks {
match block.inner {
DataGroup::ConfigurationHeader(header) => {
if configuration_header.is_some() {
return Err(ConfigurationError::MultipleConfigurationHeaders);
}
configuration_header = Some(header);
}
DataGroup::ReaderConfiguration(configuration) => {
reader_configurations.push(configuration)
}
DataGroup::SESAMVitaleComponent(component) => {
sesam_vitale_components.push(component);
}
DataGroup::PCSCReader(reader) => {
pcsc_readers.push(reader);
}
}
}
let configuration_header = match configuration_header {
Some(header) => header,
None => return Err(ConfigurationError::MissingConfigurationHeader),
};
Ok(Self {
configuration_header,
reader_configurations,
sesam_vitale_components,
pcsc_readers,
})
}
}

View File

@ -0,0 +1,3 @@
pub mod common;
pub mod configuration;
// pub mod droits_vitale;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +0,0 @@
[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

@ -1,18 +0,0 @@
//! `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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,67 +0,0 @@
<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>

View File

@ -1,24 +0,0 @@
[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
]

View File

@ -1,41 +0,0 @@
# 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
```

View File

@ -1,12 +0,0 @@
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

@ -1,35 +0,0 @@
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,
}

View File

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