44 Commits

Author SHA1 Message Date
07eae87855 WIP 2024-09-24 21:06:14 +02:00
080537fe6d feat: define structure 2024-09-24 21:06:09 +02:00
ecd84bf242 feat: add init and close library 2024-09-24 21:05:00 +02:00
981a5d34c5 feat: add first draft for crate public api 2024-09-24 21:05:00 +02:00
681cc7cb83 chore: add comment to indicate bindgen 2024-09-24 21:05:00 +02:00
fbc3be564f feat: implement parsing to data structures 2024-09-24 21:05:00 +02:00
4871187726 chore: suppress dead code warning in bindings 2024-09-24 21:05:00 +02:00
67b482ff13 feat: implement deku binary reading 2024-09-24 21:05:00 +02:00
34bdea2269 WIP commit to set working base 2024-09-24 21:04:25 +02:00
20e16f6ae2 feat: test different implementations to parse memory 2024-09-24 21:04:25 +02:00
f42e38228a chore: start implementing types from docs 2024-09-24 21:04:20 +02:00
3712667a04 chore: init sys crate 2024-09-24 21:03:14 +02:00
345190dfeb Merge pull request 'Ajout d'un parcours utilisateur⋅ice de connexion / déconnexion' (#67) from feat/61_add_login_workflow into main
Reviewed-on: P4Pillon/Krys4lide#67
Reviewed-by: kosssi <simon@p4pillon.org>

### Détails

- Ajout d'une interface "de base", avec une navbar (supprimée par la #65)
- Ajout d'un bouton de connexion, ouvrant une modale
- Sélection de l'utilisateur⋅ice au click ou par raccourci clavier
- Usage réactif d'un état partagé entre les composants, pour stocker l'information de l'utilisateur⋅ice connecté⋅e
- Menu dropdown de "profil" & Déconnexion

![Peek 24-09-2024 01-03](/attachments/4ceda5b3-26d9-4022-8923-e65a08da8dcd)

# Compatibilité

Les choix d'implémentation des éléments "dynamiques" de l'interface (modale, dropdown), encouragés par la documentation de DaisyUI, s'appuient sur les dernières évolutions de la norme HTML : il n'y a donc aucun javascript pour les gérer, c'est fait nativement par le navigateur.

Il faudrait vérifier si les librairies et framework qu'on utilise implémentent ces fonctionnements en "polyfill" pour les anciens navigateurs. Si ce n'est pas le cas, il faudra définir si :
- on cherche des polyfills adaptés
- on laisse comme ça sans rétro-compatibilité (pas très "numérique responsable")
- on fallback sur des implémentations plus "traditionnelles" mais rétro-compatibles

Closes #61
2024-09-24 12:57:34 +02:00
5712d898a5 feat: Add a client-side only user selection interface 2024-09-24 12:56:50 +02:00
3bd0a02b62 feat: implement a simple navbar 2024-09-24 12:56:50 +02:00
167a1fbbc2 Merge pull request 'Refactoring de l'interface : migration d'un monolithe HTMx vers un client Nuxt + serveur Axum' (#66) from feat/65_move_out_htmx_with_axum_backend_and_nuxt_frontend into main
Reviewed-on: P4Pillon/Krys4lide#66
Reviewed-by: kosssi <simon@p4pillon.org>

Implémentation des réflexions menées dans #65 :
- Suppression de la crate `app`, qui était un serveur axum exposant du HTMx, embedded dans le Tauri de la crate `desktop`
- Création d'un module Typescript, `frontend`, basé sur NuxtJS et générant une application statique
- Ré-écriture complète de la crate `desktop` pour encapsuler l'application statique générée par `frontend`
- Création d'une crate `backend`, serveur axum ayant pour objectif de servir de backend à l'interface, en particulier pour centraliser les accès à une base de donnée unique

- J'ai ré-utilisé TailwindCSS, mais au travers du module Nuxt dédié ; la génération est donc propre et automatisée, sans même nécessiter de configuration
- J'ai rajouté une "surcouche" à Tailwind, DaisyUI plutôt que de re-partir sur Flowbite ; ça fournit un ensemble de composants, mais de manière moins intrusive et "opinionated"

cf #65

- [Nuxt](https://nuxt.com/)
- [DaisyUI](https://daisyui.com/)
- [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/)
2024-09-24 12:53:47 +02:00
f11e2502dd feat: handle axum errors with anyhow 2024-09-23 18:56:17 +02:00
43bb2c40de feat: improve README 2024-09-23 18:56:16 +02:00
54870b0d0f feat: add the hot-reload on backend crate 2024-09-23 18:56:16 +02:00
a50d951af7 feat: setup a backend server with axum 2024-09-23 18:56:16 +02:00
2e057eee01 feat: add DaisyUI for easy components styling and dark mode handling 2024-09-23 18:56:16 +02:00
bc33bd48e8 feat: add a loader for SPA javascript loading 2024-09-23 18:56:16 +02:00
62decb3314 feat: setup tailwindcss in frontend 2024-09-23 18:56:16 +02:00
339377b838 fix: a Anyhow error handling is missing in ssvlib_demo 2024-09-23 18:56:16 +02:00
71ea6423bc fix: invalid borrowing of assets_path in get_router 2024-09-23 18:56:16 +02:00
cad2390649 feat: replace desktop by a fresh Tauri install, and add a new frontend module using Nuxt 2024-09-23 18:56:16 +02:00
ca2a0ace71 Merge pull request 'Rendre le système de fichier de configuration runtime fonctionnel en dev et en release' (#56) from fix/55_move_env_config_into_consistent_dirs into main
Reviewed-on: P4Pillon/Krys4lide#56
Reviewed-by: kosssi <simon@p4pillon.org>

# Détails

- Rajoute une librairie d'utilitaires crates/utils
- Rajoute des fonctions de gestion des fichiers et dossiers de configuration dans la lib utils, dont une fonction de chargement du fichier de config approprié
- Remplace le chargement d'un .env relatif au CARGO_MANIFEST_DIR de la librairie sesam-vitale par la fonction de chargement de config

La fonction de chargement de config génère une hiérarchie d'emplacements de fichiers de config (.env dans : dossier courant, dossier manifest, dossier système) et charge le "plus proche", afin de permettre d'avoir une configuration stable au niveau système, mais de pouvoir la surcharger facilement en local, en particulier lors de phases de développement).
Pourquoi ?

L'usage de CARGO_MANIFEST_DIR pour trouver le fichier de configuration n'était pas viable, car cette variable d'environnement n'existe que lors d'un lancement via cargo run, mais pas lors d'un appel direct à l'executable buildé.
La nouvelle implémentation est maintenant totalement compatible, autant avec des approches de surcharge en développement que pour de installations pérennes sur un système.

# Documentation

Le chemin standard des fichiers de config, spécifique à chaque OS, est obtenu à l'aide de la librairie directories-rs

Closes #55
2024-08-30 18:29:56 +02:00
f16986ce26 refacto: explicit dotenv import in sesam-vitale/build.rs 2024-08-30 18:28:29 +02:00
f56439c9c5 feat: initialize a utils lib with config functions handling config files in local and standard OS directories 2024-08-30 18:28:29 +02:00
90ff593438 Merge pull request 'Implémentation "HATEOAS" de l'interface pour HTMX et update des URLs qui fonctionne !' (#57) from feat/54_update_url_on_navbar_navigation into main
Reviewed-on: P4Pillon/Krys4lide#57
Reviewed-by: kosssi <simon@p4pillon.org>

Implémente une approche plus respectueuse de "HATEOAS" comme nécessité par HTMX.

Dans cette approche, les routes des pages, comme /index ou /cps, ont, par défaut, un rendu "complet", comme cela serait si l'on n'utilisait pas HTMX.
Certains helpers sont utilisés pour éviter la duplication de code (entre particulier une "macro" Jinja pour la navbar).

Un "Extractor" issu de axum-htmx permet d'identifier quand une requête sur ces pages est issue d'un appel htmx, et permet de générer un rendu "simplifié", ne contenant (quasiment) que les éléments HTML ayant leur contenu qui change.
Par exemple, la page /cps, dans le cadre d'une requête htmx (hx-request=true), ne retourne que : une nouvelle balise <title>, la navbar (pour changer le design de l'élément "courant") et le contenu de la page. Tout le contenu de <head> ou les div qui encapsulent l'ensemble de la page ne sont pas re-envoyé.

Cela permet de mettre à jour les URLs dans la barre du navigateur, sans provoquer les problèmes de rechargement de page identifié dans le ticket #54.

Au passage, cette PR organise les templates d'une manière qui me parait un peu plus "claire" qu'avant :

- Les "pages", correspondant à des routes réelles, sont définies dans app/src/pages
- Les "composants", comme la navbar, qui ont vocation à être "inséré" (plutôt par des includes ou des appels de fonctions), sont définis dans app/src/components

L'avantage de leur présence dans le dossier src est qu'on peut sans problème mettre à côté les fichiers .rs qui leur correspondent.

La PR #53 et le ticket #54 avaient éclairé un problème lorsqu'on souhaitait pouvoir refléter la navigation dans l'URL du navigateur. Cette PR corrige ce problème en adoptant une approche plus respectueuse de la philosophie de HTMX.

- La nouvelle librairie axum-htmx qui offre des helpers pour faire du HTMX dans Axum
- Un repo' d'exemple intéressant, du même auteur, qui propose certaines approches pour gérer les "partials" (le rendu différentiel selon qu'on a une requête HTMX ou pas)

Closes #46
Closes #54
2024-08-30 18:25:53 +02:00
216eb73757 fixup! refacto: move home code into a dedicated file and rename index to home everywhere 2024-08-27 11:28:36 +02:00
d4e565601a fix: make darkmode work by removing hardcoded tailwindcss from flowbite 2024-08-27 11:19:59 +02:00
c39ae44d74 docs: add some comments on useful locations 2024-08-27 11:19:59 +02:00
b7fcfe3792 refacto: move home code into a dedicated file and rename index to home everywhere 2024-08-27 11:19:59 +02:00
ab908f2664 fix: Small translations and misspelled IDs 2024-08-27 11:19:59 +02:00
7d4dc81df2 feat: implement htmx with partials on index and cps pages 2024-08-27 11:19:59 +02:00
2236a7219b refacto: remove old_* directories 2024-08-27 11:19:59 +02:00
3e9e8ecacc chore: update style.css 2024-08-27 11:19:59 +02:00
8ce18e53d5 feat: Rewrite routes, pages and components to be more HATEOAS 2024-08-27 11:19:59 +02:00
7487b34a17 refacto: rename html and rs templates dirs to old_* 2024-08-27 11:19:59 +02:00
217667253a feat: Add hx-push-url attribute to nav menu item 2024-08-27 11:19:59 +02:00
6dbf5b5438 feat: Add HX-Request header extraction to CPS endpoint 2024-08-27 11:19:59 +02:00
0e2e863bc0 Merge pull request 'fix: #50 - Correction des noms de module utilisés dans le template gitea de rapport de bug' (#59) from fix/50_use_correct_crates_labels_on_gitea into main
Reviewed-on: P4Pillon/Krys4lide#59
Reviewed-by: kosssi <simon@p4pillon.org>

Les noms utilisés dans le template par défaut lors d'une création d'un ticket de Bug étaient les "anciens" noms (Cargo, Tauri ...)

Cette PR :
- Ajoute quelques options (docs, scripts ...)
- Remplace les anciens noms par des labels plus compréhensifs et les noms des "crates" ou dossiers correspondant
2024-08-27 10:43:16 +02:00
307bdf8fa6 fix: update module options in BUG_REPORT.yaml template 2024-08-26 22:36:53 +02:00
81 changed files with 2283 additions and 1417 deletions

View File

@ -24,6 +24,10 @@ body:
label: Ce problème est il relatif à un ou des modules en particulier ? label: Ce problème est il relatif à un ou des modules en particulier ?
multiple: true multiple: true
options: options:
- Clego - Interface utilisateur⋅ice (crates/app)
- Tauri - Encapsulation Tauri (crates/desktop)
- Axum - Moteur SESAM-Vitale (crates/sesam-vitale)
- Librairie utilitaire (crates/utils)
- Documentation (docs)
- Scripts (scripts)
- Autre

960
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,9 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"crates/app", "crates/backend",
"crates/sesam-vitale",
"crates/desktop", "crates/desktop",
"crates/sesam-vitale",
"crates/services-sesam-vitale-sys",
"crates/utils", "crates/utils",
] ]

View File

@ -2,12 +2,14 @@
Logiciel de Pharmacie libre et open-source. Logiciel de Pharmacie libre et open-source.
## Crates ## Modules applicatifs
- `app`: Interface du logiciel, servie par un serveur web propulsé par Axum. Utilisable en mode endpoint ou encapsulé dans le client `desktop` - `crates`: Dossier racine des modules Rust
- `desktop`: Client desktop propulsé par Tauri, encapsulant le serveur web `app` - `crates/backend`: Serveur backend propulsé par Axum, exposant une API REST
- `sesam-vitale`: Bibliothèque de gestion des services SESAM-Vitale (Lecture des cartes CPS et Vitale, téléservices ...) - `crates/desktop`: Client desktop propulsé par Tauri, exposant le `frontend`
- `utils`: Bibliothèque de fonctions utilitaires - `crates/sesam-vitale`: Bibliothèque de gestion des services SESAM-Vitale (Lecture des cartes CPS et Vitale, téléservices ...)
- `crates/utils`: Bibliothèque de fonctions utilitaires
- `frontend`: Interface web du logiciel, propulsée par Nuxt.js
## Installation ## Installation
@ -26,57 +28,49 @@ Des exemples de fichiers de configuration sont disponibles à la racine du proje
### Pré-requis ### Pré-requis
#### Frontend (Nuxt + Typescript)
Le frontend est propulsé par Nuxt.js, un framework TypeScript pour Vue.js. Pour le développement, il est nécessaire d'installer les dépendances suivantes :
- [Bun](https://bun.sh/docs/installation), un gestionnaire de paquets, équivalent à `npm` en plus performant
#### Tauri CLI #### Tauri CLI
TODO: Tauri CLI, réellement nécessaire ?
La CLI Tauri est nécessaire au lancement du client `desktop`. Elle peut être installée via Cargo : La CLI Tauri est nécessaire au lancement du client `desktop`. Elle peut être installée via Cargo :
```bash ```bash
cargo install tauri-cli --version "^2.0.0-beta" cargo install tauri-cli --version "^2.0.0-rc"
``` ```
#### Tailwindcss CLI
Le CLI Tailwindcss est nécessaire pour la génération du fichier `crates/app/assets/css/style.css`.
La documentation d'installation est disponible sur le site officiel de Tailwindcss : https://tailwindcss.com/blog/standalone-cli
La version actuellement utilisée est la [`v3.4.7`](https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.4.7)
#### SESAM-Vitale #### 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`. 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`.
#### Backend Hot-reload
Voir le [README](crates/backend/README.md) de la crate `backend` pour les prérequis de développement du serveur backend.
### Lancement ### Lancement
Le logiciel dans sa globalité peut être lancé via la commande suivante : Pour lancer l'application en mode développement, il est nécessaire d'exécuter plusieurs composants simultanément :
```bash ```bash
# Lancement du serveur backend
systemfd --no-pid -s http::3030 -- cargo watch -x 'run --bin backend'
```
```bash
# Lancement de l'interface utilisateur (frontend ou desktop)
# - frontend (serveur web, accessible via navigateur)
bun run --cwd frontend/ dev
# - desktop (client desktop, basé sur Tauri)
cargo tauri dev cargo tauri dev
``` ```
/!\ Attention, le lancement du client `desktop` ne génère pas le fichier `crates/app/assets/css/style.css` automatiquement pour le moment. En cas de modification des interfaces web, il est donc nécessaire de procéder à sa génération comme indiqué dans le [README](crates/app/README.md) de la crate `app`.
Si vous souhaitez lancer les composants séparément, les indications de lancement sont disponibles dans les README des différents crates.
- [app](crates/app/README.md)
- [sesam-vitale](crates/sesam-vitale/README.md)
## Rechargement automatique
Pour permettre de développer plus rapidement, il existe une librairie qui recompile automatiquement nos modifications en cours : [`cargo-watch`](https://github.com/watchexec/cargo-watch) permet de relancer une commande `cargo` lorsqu'un fichier est modifié (example: `cargo run` --> `cargo watch -x run`).
Voici la commande pour l'installer dans un _package_ :
```bash
cargo add cargo-watch --dev --package app
```
Le fichier [`.ignore`](./ignore) permet d'ignorer certains fichiers pour éviter de relancer la recompilation inutilement.
⚠️ La librairie n'est pas compatible avec _Windows 7_ et les versions antérieurs de _Windows_.
## Build ## Build
Packager le client desktop 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 :
```bash ```bash
cargo tauri build cargo tauri build

View File

@ -7,6 +7,7 @@ edition = "2021"
askama = "0.12.1" askama = "0.12.1"
askama_axum = "0.4.0" askama_axum = "0.4.0"
axum = "0.7.5" axum = "0.7.5"
axum-htmx = { version = "0.6", features = ["auto-vary"] }
listenfd = "1.0.1" listenfd = "1.0.1"
notify = "6.1.1" notify = "6.1.1"
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }

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 one or more lines are too long

View File

@ -566,28 +566,8 @@ video {
border-width: 0; border-width: 0;
} }
.absolute { .z-50 {
position: absolute; z-index: 50;
}
.relative {
position: relative;
}
.-inset-0\.5 {
inset: -0.125rem;
}
.-inset-1\.5 {
inset: -0.375rem;
}
.right-0 {
right: 0px;
}
.z-10 {
z-index: 10;
} }
.mx-auto { .mx-auto {
@ -595,36 +575,9 @@ video {
margin-right: auto; margin-right: auto;
} }
.-mr-2 { .my-4 {
margin-right: -0.5rem;
}
.ml-3 {
margin-left: 0.75rem;
}
.ml-auto {
margin-left: auto;
}
.mt-2 {
margin-top: 0.5rem;
}
.mt-3 {
margin-top: 0.75rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mt-4 {
margin-top: 1rem; margin-top: 1rem;
} margin-bottom: 1rem;
.mt-6 {
margin-top: 1.5rem;
} }
.mb-2 { .mb-2 {
@ -635,10 +588,22 @@ video {
margin-bottom: 0.625rem; margin-bottom: 0.625rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.me-3 { .me-3 {
margin-inline-end: 0.75rem; margin-inline-end: 0.75rem;
} }
.mt-3 {
margin-top: 0.75rem;
}
.mt-4 {
margin-top: 1rem;
}
.block { .block {
display: block; display: block;
} }
@ -663,52 +628,44 @@ video {
height: 2.5rem; height: 2.5rem;
} }
.h-16 { .h-2 {
height: 4rem; height: 0.5rem;
}
.h-6 {
height: 1.5rem;
}
.h-8 {
height: 2rem;
}
.h-full {
height: 100%;
}
.h-32 {
height: 8rem;
}
.h-48 {
height: 12rem;
}
.h-96 {
height: 24rem;
} }
.h-2\.5 { .h-2\.5 {
height: 0.625rem; height: 0.625rem;
} }
.h-9 { .h-32 {
height: 2.25rem; height: 8rem;
}
.h-4 {
height: 1rem;
}
.h-48 {
height: 12rem;
}
.h-5 {
height: 1.25rem;
} }
.h-7 { .h-7 {
height: 1.75rem; height: 1.75rem;
} }
.h-2 { .h-8 {
height: 0.5rem; height: 2rem;
} }
.h-4 { .h-96 {
height: 1rem; height: 24rem;
}
.h-full {
height: 100%;
} }
.min-h-full { .min-h-full {
@ -719,58 +676,38 @@ video {
width: 2.5rem; width: 2.5rem;
} }
.w-32 {
width: 8rem;
}
.w-48 { .w-48 {
width: 12rem; width: 12rem;
} }
.w-6 { .w-5 {
width: 1.5rem; width: 1.25rem;
} }
.w-8 { .w-8 {
width: 2rem; width: 2rem;
} }
.w-auto { .w-full {
width: auto; width: 100%;
}
.w-32 {
width: 8rem;
}
.w-20 {
width: 5rem;
}
.w-24 {
width: 6rem;
}
.w-28 {
width: 7rem;
} }
.max-w-7xl { .max-w-7xl {
max-width: 80rem; max-width: 80rem;
} }
.max-w-xs { .max-w-screen-xl {
max-width: 20rem; max-width: 1280px;
} }
.max-w-sm { .max-w-sm {
max-width: 24rem; max-width: 24rem;
} }
.flex-shrink-0 {
flex-shrink: 0;
}
.origin-top-right {
transform-origin: top right;
}
@keyframes pulse { @keyframes pulse {
50% { 50% {
opacity: .5; opacity: .5;
@ -781,12 +718,20 @@ video {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
} }
.list-none {
list-style-type: none;
}
.grid-cols-1 { .grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr)); grid-template-columns: repeat(1, minmax(0, 1fr));
} }
.grid-cols-2 { .flex-col {
grid-template-columns: repeat(2, minmax(0, 1fr)); flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
} }
.items-center { .items-center {
@ -805,48 +750,66 @@ video {
gap: 1rem; gap: 1rem;
} }
.space-y-1 > :not([hidden]) ~ :not([hidden]) { .space-x-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0; --tw-space-x-reverse: 0;
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); margin-right: calc(0.75rem * var(--tw-space-x-reverse));
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
} }
.rounded-full { .divide-y > :not([hidden]) ~ :not([hidden]) {
border-radius: 9999px; --tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
} }
.rounded-md { .divide-gray-100 > :not([hidden]) ~ :not([hidden]) {
border-radius: 0.375rem; --tw-divide-opacity: 1;
border-color: rgb(243 244 246 / var(--tw-divide-opacity));
} }
.rounded-lg { .self-center {
border-radius: 0.5rem; align-self: center;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.whitespace-nowrap {
white-space: nowrap;
} }
.rounded { .rounded {
border-radius: 0.25rem; border-radius: 0.25rem;
} }
.border-2 { .rounded-full {
border-width: 2px; border-radius: 9999px;
}
.rounded-lg {
border-radius: 0.5rem;
} }
.border { .border {
border-width: 1px; border-width: 1px;
} }
.border-b { .border-2 {
border-bottom-width: 1px; border-width: 2px;
}
.border-t {
border-top-width: 1px;
} }
.border-dashed { .border-dashed {
border-style: dashed; border-style: dashed;
} }
.border-gray-100 {
--tw-border-opacity: 1;
border-color: rgb(243 244 246 / var(--tw-border-opacity));
}
.border-gray-200 { .border-gray-200 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity)); border-color: rgb(229 231 235 / var(--tw-border-opacity));
@ -857,9 +820,9 @@ video {
border-color: rgb(209 213 219 / var(--tw-border-opacity)); border-color: rgb(209 213 219 / var(--tw-border-opacity));
} }
.bg-white { .bg-blue-700 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(29 78 216 / var(--tw-bg-opacity));
} }
.bg-gray-200 { .bg-gray-200 {
@ -872,8 +835,19 @@ video {
background-color: rgb(209 213 219 / var(--tw-bg-opacity)); background-color: rgb(209 213 219 / var(--tw-bg-opacity));
} }
.p-1 { .bg-gray-50 {
padding: 0.25rem; --tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
} }
.p-2 { .p-2 {
@ -884,36 +858,39 @@ video {
padding: 1rem; padding: 1rem;
} }
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.px-4 { .px-4 {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
} }
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.py-10 { .py-10 {
padding-top: 2.5rem; padding-top: 2.5rem;
padding-bottom: 2.5rem; padding-bottom: 2.5rem;
} }
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.py-8 { .py-8 {
padding-top: 2rem; padding-top: 2rem;
padding-bottom: 2rem; padding-bottom: 2rem;
} }
.pb-3 { .text-2xl {
padding-bottom: 0.75rem; font-size: 1.5rem;
} line-height: 2rem;
.pt-2 {
padding-top: 0.5rem;
}
.pt-4 {
padding-top: 1rem;
} }
.text-3xl { .text-3xl {
@ -939,6 +916,10 @@ video {
font-weight: 500; font-weight: 500;
} }
.font-semibold {
font-weight: 600;
}
.leading-tight { .leading-tight {
line-height: 1.25; line-height: 1.25;
} }
@ -947,9 +928,9 @@ video {
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
.text-gray-400 { .text-gray-200 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity)); color: rgb(229 231 235 / var(--tw-text-opacity));
} }
.text-gray-500 { .text-gray-500 {
@ -957,9 +938,9 @@ video {
color: rgb(107 114 128 / var(--tw-text-opacity)); color: rgb(107 114 128 / var(--tw-text-opacity));
} }
.text-gray-800 { .text-gray-700 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(31 41 55 / var(--tw-text-opacity)); color: rgb(55 65 81 / var(--tw-text-opacity));
} }
.text-gray-900 { .text-gray-900 {
@ -967,19 +948,9 @@ video {
color: rgb(17 24 39 / var(--tw-text-opacity)); color: rgb(17 24 39 / var(--tw-text-opacity));
} }
.text-gray-200 { .text-white {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
}
.opacity-0 {
opacity: 0;
}
.shadow-lg {
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.shadow { .shadow {
@ -988,31 +959,11 @@ video {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.ring-1 {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.ring-black {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity));
}
.ring-opacity-5 {
--tw-ring-opacity: 0.05;
}
.hover\:bg-gray-100:hover { .hover\:bg-gray-100:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity)); background-color: rgb(243 244 246 / var(--tw-bg-opacity));
} }
.hover\:text-gray-500:hover {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.focus\:outline-none:focus { .focus\:outline-none:focus {
outline: 2px solid transparent; outline: 2px solid transparent;
outline-offset: 2px; outline-offset: 2px;
@ -1024,47 +975,27 @@ video {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
} }
.focus\:ring-indigo-500:focus { .focus\:ring-4:focus {
--tw-ring-opacity: 1; --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity)); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
} }
.focus\:ring-offset-2:focus { .focus\:ring-gray-200:focus {
--tw-ring-offset-width: 2px; --tw-ring-opacity: 1;
--tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity));
}
.focus\:ring-gray-300:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity));
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:-my-px {
margin-top: -1px;
margin-bottom: -1px;
}
.sm\:ml-6 {
margin-left: 1.5rem;
}
.sm\:flex {
display: flex;
}
.sm\:hidden {
display: none;
}
.sm\:grid-cols-2 { .sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.sm\:items-center {
align-items: center;
}
.sm\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(2rem * var(--tw-space-x-reverse));
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
}
.sm\:px-6 { .sm\:px-6 {
padding-left: 1.5rem; padding-left: 1.5rem;
padding-right: 1.5rem; padding-right: 1.5rem;
@ -1072,28 +1003,91 @@ video {
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.md\:order-1 {
order: 1;
}
.md\:order-2 {
order: 2;
}
.md\:me-0 {
margin-inline-end: 0px;
}
.md\:mt-0 {
margin-top: 0px;
}
.md\:flex {
display: flex;
}
.md\:hidden {
display: none;
}
.md\:h-64 { .md\:h-64 {
height: 16rem; height: 16rem;
} }
.md\:h-72 { .md\:w-auto {
height: 18rem; width: auto;
}
.md\:flex-row {
flex-direction: row;
}
.md\:space-x-0 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0px * var(--tw-space-x-reverse));
margin-left: calc(0px * calc(1 - var(--tw-space-x-reverse)));
}
.md\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(2rem * var(--tw-space-x-reverse));
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
}
.md\:border-0 {
border-width: 0px;
}
.md\:bg-transparent {
background-color: transparent;
}
.md\:bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.md\:p-0 {
padding: 0px;
} }
.md\:p-6 { .md\:p-6 {
padding: 1.5rem; padding: 1.5rem;
} }
.md\:text-blue-700 {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
.md\:hover\:bg-transparent:hover {
background-color: transparent;
}
.md\:hover\:text-blue-700:hover {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.lg\:block {
display: block;
}
.lg\:hidden {
display: none;
}
.lg\:grid-cols-4 { .lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
@ -1104,7 +1098,16 @@ video {
} }
} }
.rtl\:space-x-reverse:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 1;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.dark\:divide-gray-600 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-divide-opacity));
}
.dark\:border-gray-600 { .dark\:border-gray-600 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity)); border-color: rgb(75 85 99 / var(--tw-border-opacity));
@ -1120,6 +1123,26 @@ video {
background-color: rgb(55 65 81 / var(--tw-bg-opacity)); background-color: rgb(55 65 81 / var(--tw-bg-opacity));
} }
.dark\:bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
.dark\:bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.dark\:text-gray-200 {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
}
.dark\:text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
}
.dark\:text-gray-600 { .dark\:text-gray-600 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity)); color: rgb(75 85 99 / var(--tw-text-opacity));
@ -1129,4 +1152,52 @@ video {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity)); color: rgb(55 65 81 / var(--tw-text-opacity));
} }
.dark\:text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:hover\:bg-gray-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-gray-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.dark\:hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:focus\:ring-gray-600:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity));
}
}
@media (min-width: 768px) {
@media (prefers-color-scheme: dark) {
.md\:dark\:bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.md\:dark\:text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
.md\:dark\:hover\:bg-transparent:hover {
background-color: transparent;
}
.md\:dark\:hover\:text-blue-500:hover {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
}
} }

View File

@ -1,3 +1,7 @@
{% if hx_request %}
<title>{% block title %}{{ title }}{% endblock %}</title>
{% block body %}{% endblock %}
{% else %}
<!doctype html> <!doctype html>
<html lang="fr" class="h-full"> <html lang="fr" class="h-full">
<head> <head>
@ -5,19 +9,15 @@
<script src="/assets/js/htmx@2.0.1.min.js"></script> <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/alpinejs@3.14.1.min.js" defer></script>
<link href="/assets/css/style.css" rel="stylesheet">
<link href="/assets/css/flowbite@2.5.1.min.css" rel="stylesheet" />
<script src="/assets/js/flowbite@2.5.1.min.js"></script> <script src="/assets/js/flowbite@2.5.1.min.js"></script>
<link href="/assets/css/style.css" rel="stylesheet">
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body class="h-full"> <body class="h-full">
<div class="min-h-full"> <div class="min-h-full">
{% block nav %}
{% include "layout/nav.html" %}
{% endblock %}
{% block body %}{% endblock %} {% block body %}{% endblock %}
</div> </div>
</body> </body>
</html> </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

@ -1,29 +1,21 @@
mod pages; use std::path::PathBuf;
mod templates;
use std::path::Path;
use askama_axum::Template;
use axum::http::{StatusCode, Uri}; use axum::http::{StatusCode, Uri};
use axum_htmx::AutoVaryLayer;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
mod menu;
mod pages;
async fn fallback(uri: Uri) -> (StatusCode, String) { async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}")) (StatusCode::NOT_FOUND, format!("No route for {uri}"))
} }
#[derive(Template)] pub async fn get_router(assets_path: PathBuf) -> axum::Router<()> {
#[template(path = "index.html")]
pub struct GetIndexTemplate;
async fn root() -> GetIndexTemplate {
GetIndexTemplate {}
}
pub fn get_router(assets_path: &Path) -> axum::Router {
axum::Router::new() axum::Router::new()
.nest_service("/assets", ServeDir::new(assets_path)) .nest_service("/assets", ServeDir::new(assets_path))
.route("/", axum::routing::get(root)) .merge(pages::get_routes())
.nest("/pages", pages::get_routes())
.merge(templates::get_routes())
.fallback(fallback) .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,15 +1,17 @@
use ::app::get_router; use std::path::{Path, PathBuf};
use std::{env, io};
use axum::body::Body; use axum::body::Body;
use axum::http::Request; use axum::http::Request;
use listenfd::ListenFd; use listenfd::ListenFd;
use notify::Watcher; use notify::Watcher;
use std::path::Path;
use std::{env, io};
use thiserror::Error; use thiserror::Error;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_livereload::predicate::Predicate; use tower_livereload::predicate::Predicate;
use tower_livereload::LiveReloadLayer; use tower_livereload::LiveReloadLayer;
use ::app::get_router;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum AppError { pub enum AppError {
#[error("Unable to bind to TCP listener")] #[error("Unable to bind to TCP listener")]
@ -46,12 +48,14 @@ async fn get_tcp_listener() -> Result<TcpListener, io::Error> {
} }
fn get_livereload_layer( fn get_livereload_layer(
templates_path: &Path, templates_paths: Vec<PathBuf>,
) -> Result<LiveReloadLayer<NotHtmxPredicate>, notify::Error> { ) -> Result<LiveReloadLayer<NotHtmxPredicate>, notify::Error> {
let livereload = LiveReloadLayer::new(); let livereload = LiveReloadLayer::new();
let reloader = livereload.reloader(); let reloader = livereload.reloader();
let mut watcher = notify::recommended_watcher(move |_| reloader.reload())?; let mut watcher = notify::recommended_watcher(move |_| reloader.reload())?;
watcher.watch(templates_path, notify::RecursiveMode::Recursive)?; for templates_path in templates_paths {
watcher.watch(templates_path.as_path(), notify::RecursiveMode::Recursive)?;
}
Ok(livereload.request_predicate::<Body, NotHtmxPredicate>(NotHtmxPredicate)) Ok(livereload.request_predicate::<Body, NotHtmxPredicate>(NotHtmxPredicate))
} }
@ -61,11 +65,14 @@ async fn main() -> Result<(), AppError> {
var: "CARGO_MANIFEST_DIR", var: "CARGO_MANIFEST_DIR",
})?; })?;
let assets_path = Path::new(&manifest_dir).join("assets"); let assets_path = Path::new(&manifest_dir).join("assets");
let templates_path = Path::new(&manifest_dir).join("templates"); let templates_paths = vec![
Path::new(&manifest_dir).join("src/pages"),
Path::new(&manifest_dir).join("src/components"),
];
let livereload_layer = let livereload_layer =
get_livereload_layer(&templates_path).map_err(AppError::NotifyWatcher)?; get_livereload_layer(templates_paths).map_err(AppError::NotifyWatcher)?;
let router = get_router(assets_path.as_path()).layer(livereload_layer); let router = get_router(assets_path).await.layer(livereload_layer);
let listener: TcpListener = get_tcp_listener().await.map_err(AppError::TCPListener)?; let listener: TcpListener = get_tcp_listener().await.map_err(AppError::TCPListener)?;
let local_addr = listener.local_addr().map_err(AppError::TCPListener)?; let local_addr = listener.local_addr().map_err(AppError::TCPListener)?;

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

@ -1,9 +1,12 @@
use askama_axum::Template; use askama_axum::Template;
use axum_htmx::HxRequest;
#[derive(Template)] #[derive(Template)]
#[template(path = "pages/cps.html")] #[template(path = "cps.html")]
pub struct CpsTemplate; pub struct CpsTemplate {
hx_request: bool,
pub async fn cps() -> CpsTemplate { }
CpsTemplate
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

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

View File

@ -5,6 +5,6 @@ mod home;
pub fn get_routes() -> Router { pub fn get_routes() -> Router {
Router::new() Router::new()
.route("/home", routing::get(home::home)) .route("/", routing::get(home::home))
.route("/cps", routing::get(cps::cps)) .route("/cps", routing::get(cps::cps))
} }

View File

@ -1,18 +0,0 @@
use askama_axum::Template;
use axum::{routing, Router};
#[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate {
pub name: String,
}
async fn hello() -> HelloTemplate {
HelloTemplate {
name: "Theo".to_string(),
}
}
pub fn get_routes() -> Router {
Router::new().route("/", routing::get(hello))
}

View File

@ -1,12 +0,0 @@
use axum::Router;
mod hello;
mod nav;
mod profile;
pub fn get_routes() -> Router {
Router::new()
.nest("/hello", hello::get_routes())
.nest("/nav", nav::get_routes())
.nest("/profile", profile::get_routes())
}

View File

@ -1,60 +0,0 @@
use askama_axum::Template;
use axum::{extract::Query, routing, Router};
use serde::Deserialize;
struct MenuItem {
label: String,
href: String,
current: bool,
}
#[derive(Deserialize)]
struct MenuParameters {
mobile: bool,
}
#[derive(Template)]
#[template(path = "layout/nav/nav-menu-items.html")]
struct MenuTemplate {
mobile: bool,
items: Vec<MenuItem>,
}
impl MenuTemplate {
fn get_classes(&self, is_current_item: &bool) -> String {
let common_classes = match self.mobile {
true => "block border-l-4 py-2 pl-3 pr-4 text-base font-medium".to_string(),
false => {
"inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium".to_string()
}
};
match (self.mobile, is_current_item) {
(true, true) => common_classes + " border-indigo-500 bg-indigo-50 text-indigo-700",
(true, false) => common_classes + " border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800",
(false, true) => common_classes + " border-indigo-500 text-gray-900",
(false, false) => common_classes + " border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
}
}
}
async fn menu(Query(params): Query<MenuParameters>) -> MenuTemplate {
MenuTemplate {
mobile: params.mobile,
items: vec![
MenuItem {
label: "Accueil".to_string(),
href: "/pages/home".to_string(),
current: true,
},
MenuItem {
label: "CPS".to_string(),
href: "/pages/cps".to_string(),
current: false,
},
],
}
}
pub fn get_routes() -> Router {
Router::new().route("/menu", routing::get(menu))
}

View File

@ -1,63 +0,0 @@
use askama_axum::Template;
use axum::{extract::Query, routing, Router};
use serde::Deserialize;
struct MenuItem {
label: String,
id: String,
current: bool,
}
#[derive(Deserialize)]
struct MenuParameters {
mobile: bool,
}
#[derive(Template)]
#[template(path = "layout/nav/profile-menu-items.html")]
struct MenuTemplate {
mobile: bool,
items: Vec<MenuItem>,
}
impl MenuTemplate {
fn get_classes(&self, is_current_item: &bool) -> String {
let common_classes = match self.mobile {
true => "block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800".to_string(),
false => "block px-4 py-2 text-sm text-gray-700".to_string(),
};
match (self.mobile, is_current_item) {
(true, true) => common_classes + "", // ???
(true, false) => common_classes + "",
(false, true) => common_classes + " bg-gray-100",
(false, false) => common_classes + "",
}
}
}
async fn menu(Query(params): Query<MenuParameters>) -> MenuTemplate {
MenuTemplate {
mobile: params.mobile,
items: vec![
MenuItem {
label: "Votre profil".to_string(),
id: "profile".to_string(),
current: false,
},
MenuItem {
label: "Paramètres".to_string(),
id: "settings".to_string(),
current: false,
},
MenuItem {
label: "Déconnexion".to_string(),
id: "logout".to_string(),
current: false,
},
],
}
}
pub fn get_routes() -> Router {
Router::new().route("/menu", routing::get(menu))
}

View File

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

View File

@ -1 +0,0 @@
<div>Hello {{name}}!</div>

View File

@ -1,34 +0,0 @@
{% extends "base.html" %}
{% block title %}Pharma Libre{% endblock %}
{% block body %}
<div class="py-10">
<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"
>
{% include "skeletons/page-title.html" %}
</h1>
</div>
</header>
<main>
<div
id="main-container"
class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"
>
<!-- Your content -->
<div
hx-get="/pages/home"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
{% include "skeletons/card.html" %}
</div>
</div>
</main>
</div>
{% endblock %}

View File

@ -1,41 +0,0 @@
<nav
class="border-b border-gray-200 bg-white"
x-data="{ menuOpen: false }"
>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-between">
<div class="flex">
{% include "layout/nav/logo.html" %}
<div class="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8">
{% include "layout/nav/desktop/menu-items.html" %}
</div>
</div>
<div class="hidden sm:ml-6 sm:flex sm:items-center">
{% include "layout/nav/desktop/notifications-button.html" %}
{% include "layout/nav/desktop/profile.html" %}
</div>
<div class="-mr-2 flex items-center sm:hidden">
{% include "layout/nav/mobile/menu-button.html" %}
</div>
</div>
</div>
<!-- Mobile menu, show/hide based on menu state. -->
<div
class="sm:hidden" id="mobile-menu"
x-show="menuOpen"
x-cloak
>
<div class="space-y-1 pb-3 pt-2">
{% include "layout/nav/mobile/menu-items.html" %}
</div>
<div class="border-t border-gray-200 pb-3 pt-4">
<div class="flex items-center px-4">
{% include "layout/nav/mobile/profile.html" %}
{% include "layout/nav/mobile/notifications-button.html" %}
</div>
<div class="mt-3 space-y-1">
{% include "layout/nav/mobile/profile-items.html" %}
</div>
</div>
</div>
</nav>

View File

@ -1,9 +0,0 @@
<div
id="nav-menu-desktop"
hx-get="/nav/menu?mobile=false"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
{% include "skeletons/menu-items.html" %}
</div>

View File

@ -1,6 +0,0 @@
<button
type="button"
class="relative rounded-full bg-white p-1 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
{% include "layout/nav/notifications-icon.html" %}
</button>

View File

@ -1,22 +0,0 @@
<div
class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu"
id="profile-dropdown"
aria-orientation="vertical"
aria-labelledby="user-menu-button"
tabindex="-1"
x-show="profileOpen"
x-on:click.outside="profileOpen = false"
x-cloak
x-transition
>
<div
id="profile-menu-desktop"
hx-get="/profile/menu?mobile=false"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
Chargement ...
</div>
</div>

View File

@ -1,27 +0,0 @@
<!-- Profile dropdown -->
<div
class="relative ml-3"
x-data="{ profileOpen: false }"
>
<div>
<button
type="button"
class="relative flex max-w-xs items-center rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
id="user-menu-button"
aria-controls="profile-dropdown"
aria-expanded="false"
aria-haspopup="menu"
x-on:click="profileOpen = ! profileOpen"
>
<span class="absolute -inset-1.5"></span>
<span class="sr-only">Open user menu</span>
<img
class="h-8 w-8 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</button>
</div>
{% include "layout/nav/desktop/profile-dropdown.html" %}
</div>

View File

@ -1,4 +0,0 @@
<div class="flex flex-shrink-0 items-center">
<img class="block h-8 w-auto lg:hidden" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company">
<img class="hidden h-8 w-auto lg:block" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company">
</div>

View File

@ -1,43 +0,0 @@
<!-- Mobile menu button -->
<button
type="button"
class="relative inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
aria-controls="mobile-menu"
x-on:click="menuOpen = ! menuOpen"
x-bind:aria-expanded="menuOpen"
>
<span class="absolute -inset-0.5"></span>
<span class="sr-only">Open main menu</span>
<!-- Menu open: "hidden", Menu closed: "block" -->
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
x-bind:class="menuOpen ? 'hidden' : 'block'"
x-bind:aria-hidden="menuOpen"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
<!-- Menu open: "block", Menu closed: "hidden" -->
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
x-bind:class="menuOpen ? 'block' : 'hidden'"
x-bind:aria-hidden="! menuOpen"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>

View File

@ -1,9 +0,0 @@
<div
id="nav-menu-mobile"
hx-get="/nav/menu?mobile=true"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
Chargement ...
</div>

View File

@ -1,6 +0,0 @@
<button
type="button"
class="relative ml-auto flex-shrink-0 rounded-full bg-white p-1 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
{% include "layout/nav/notifications-icon.html" %}
</button>

View File

@ -1,9 +0,0 @@
<div
id="profile-menu-mobile"
hx-get="/profile/menu?mobile=true"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
Chargement ...
</div>

View File

@ -1,11 +0,0 @@
<div class="flex-shrink-0">
<img
class="h-10 w-10 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</div>
<div class="ml-3">
<div class="text-base font-medium text-gray-800">Tom Cook</div>
<div class="text-sm font-medium text-gray-500">tom@example.com</div>
</div>

View File

@ -1,13 +0,0 @@
{% for item in items %}
<a
href=""
hx-get="{{ item.href }}"
hx-trigger="click"
hx-target="#main-container"
hx-swap="innerHTML"
class="{{ Self::get_classes(self, item.current) }}"
aria-current="{% if item.current %}page{% endif %}"
>
{{ item.label }}
</a>
{% endfor %}

View File

@ -1,16 +0,0 @@
<span class="absolute -inset-1.5"></span>
<span class="sr-only">View notifications</span>
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"
/>
</svg>

View File

@ -1,11 +0,0 @@
{% for item in items %}
<a
href="#{{ item.id }}"
class="{{ Self::get_classes(self, item.current) }}"
role="menuitem"
tabindex="-1"
id="{{ item.id }}"
>
{{ item.label }}
</a>
{% endfor %}

View File

@ -1,52 +0,0 @@
<h3 id="page-title" hx-swap-oob="textContent">
CPS
</h3>
<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"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
></div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
></div>
<div class="grid grid-cols-2 gap-4">
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
</div>

View File

@ -1,52 +0,0 @@
<h3 id="page-title" hx-swap-oob="textContent">
Accueil
</h3>
<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"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
></div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
></div>
<div class="grid grid-cols-2 gap-4">
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
</div>

14
crates/backend/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "backend"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.89"
axum = "0.7.6"
listenfd = "1.0.1"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }
[dev-dependencies]
cargo-watch = "8.5.2"
systemfd = "0.4.3"

19
crates/backend/README.md Normal file
View File

@ -0,0 +1,19 @@
# Backend
Ceci est un serveur backend, basé sur axum, et permettant d'offrir une gestion centralisée des accès aux données.
## Prérequis
En développement, le mécanisme de hot-reload nécessite de disposer de `cargo-watch` et `systemfd`. Pour les installer, exécutez la commande suivante :
```bash
cargo install cargo-watch systemfd
```
## Développement
Pour lancer le serveur en mode développement, exécutez la commande suivante :
```bash
systemfd --no-pid -s http::3030 -- cargo watch -x 'run --bin backend'
```

37
crates/backend/src/lib.rs Normal file
View File

@ -0,0 +1,37 @@
use anyhow::Error as AnyError;
use axum::http::{StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use axum::{routing::get, Router};
pub fn get_router() -> Router {
Router::new()
.route("/", get(|| async { "Hello, world!" }))
.fallback(fallback)
}
async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
}
struct AppError(AnyError);
// To automatically convert `AppError` into a response
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Internal Server Error: {}", self.0),
)
.into_response()
}
}
// To automatically convert `AnyError` into `AppError`
impl<E> From<E> for AppError
where
E: Into<AnyError>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}

View File

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

View File

@ -10,16 +10,11 @@ name = "desktop_lib"
crate-type = ["lib", "cdylib", "staticlib"] crate-type = ["lib", "cdylib", "staticlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-beta", features = [] } tauri-build = { version = "2.0.0-rc", features = [] }
[dependencies] [dependencies]
axum = "0.7.5" tauri = { version = "2.0.0-rc", features = [] }
tauri = { version = "2.0.0-beta", features = [] } tauri-plugin-shell = "2.0.0-rc"
tower = "0.4.13" serde = { version = "1", features = ["derive"] }
tokio = "1.39.1" serde_json = "1"
app = { path = "../app" }
http = "1.1.0"
bytes = "1.6.1"
thiserror = "1.0.63"

View File

@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open"
]
}

View File

@ -1,88 +1,14 @@
use axum::body::{to_bytes, Body}; // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
use axum::Router; #[tauri::command]
use bytes::Bytes; fn greet(name: &str) -> String {
use http::{request, response, Request, Response}; format!("Hello, {}! You've been greeted from Rust!", name)
use std::path::PathBuf;
use std::sync::Arc;
use tauri::path::BaseDirectory;
use tauri::Manager;
use thiserror::Error;
use tokio::sync::{Mutex, MutexGuard};
use tower::{Service, ServiceExt};
#[derive(Error, Debug)]
pub enum DesktopError {
#[error("Axum error:\n{0}")]
Axum(#[from] axum::Error),
#[error("Infallible error")]
Infallible(#[from] std::convert::Infallible),
}
/// Process requests sent to Tauri (with the `axum://` protocol) and handle them with Axum
/// When an error occurs, this function is expected to panic, which should result in a 500 error
/// being sent to the client, so we let the client handle the error recovering
async fn process_tauri_request(
tauri_request: Request<Vec<u8>>,
mut router: MutexGuard<'_, Router>,
) -> Result<Response<Vec<u8>>, DesktopError> {
let (parts, body): (request::Parts, Vec<u8>) = tauri_request.into_parts();
let axum_request: Request<Body> = Request::from_parts(parts, body.into());
let axum_response: Response<Body> = router
.as_service()
.ready()
.await
.map_err(DesktopError::Infallible)?
.call(axum_request)
.await
.map_err(DesktopError::Infallible)?;
let (parts, body): (response::Parts, Body) = axum_response.into_parts();
let body: Bytes = to_bytes(body, usize::MAX).await?;
let tauri_response: Response<Vec<u8>> = Response::from_parts(parts, body.into());
Ok(tauri_response)
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.setup(|app| { .plugin(tauri_plugin_shell::init())
let assets_path: PathBuf = app .invoke_handler(tauri::generate_handler![greet])
.path()
.resolve("assets", BaseDirectory::Resource)
.expect("Assets path should be resolvable");
// Adds Axum router to application state
// This makes it so we can retrieve it from any app instance (see bellow)
let router = Arc::new(Mutex::new(app::get_router(&assets_path)));
app.manage(router);
Ok(())
})
.register_asynchronous_uri_scheme_protocol("axum", move |app, request, responder| {
// Retrieve the router from the application state and clone it for the async block
let router = Arc::clone(&app.state::<Arc<Mutex<axum::Router>>>());
// Spawn a new async task to process the request
tauri::async_runtime::spawn(async move {
let router = router.lock().await;
match process_tauri_request(request, router).await {
Ok(response) => responder.respond(response),
Err(err) => {
let body = format!("Failed to process an axum:// request:\n{}", err);
responder.respond(
http::Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CONTENT_TYPE, "text/plain")
.body::<Vec<u8>>(body.into())
.expect("BAD_REQUEST response should be valid"),
)
}
}
});
})
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -1,20 +1,24 @@
{ {
"productName": "Logiciel Pharma", "$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "Chrys4lide LGO",
"version": "0.0.1", "version": "0.0.1",
"identifier": "org.p4pillon.pharma.desktop", "identifier": "org.p4pillon.chrys4lide.lgo",
"build": { "build": {
"beforeDevCommand": { "beforeDevCommand": {
"cwd": "../app", "cwd": "../../frontend",
"script": "cargo run" "script": "bun run dev"
}, },
"devUrl": "http://localhost:3000", "devUrl": "http://localhost:1420",
"frontendDist": "axum://place.holder/" "beforeBuildCommand": {
"cwd": "../../frontend",
"script": "bun run generate"
},
"frontendDist": "../../frontend/dist"
}, },
"app": { "app": {
"withGlobalTauri": true,
"windows": [ "windows": [
{ {
"title": "Logiciel Pharma", "title": "Chrys4lide | LG0",
"width": 800, "width": 800,
"height": 600 "height": 600
} }
@ -25,9 +29,6 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"resources": {
"../app/assets/": "./assets/"
},
"targets": "all", "targets": "all",
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
@ -37,5 +38,4 @@
"icons/icon.ico" "icons/icon.ico"
] ]
} }
}
}

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

@ -17,6 +17,8 @@ pub enum SSVDemoError {
CartePSReading(#[from] crate::cps::CartePSError), CartePSReading(#[from] crate::cps::CartePSError),
#[error(transparent)] #[error(transparent)]
SSVLibErrorCode(#[from] crate::libssv::LibSSVError), SSVLibErrorCode(#[from] crate::libssv::LibSSVError),
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
} }
fn ssv_init_lib_2() -> Result<(), SSVDemoError> { fn ssv_init_lib_2() -> Result<(), SSVDemoError> {

24
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

77
frontend/README.md Normal file
View File

@ -0,0 +1,77 @@
# Nuxt 3 Minimal Starter
TODO : Faire un vrai README pour `frontend` (Nuxt 3)
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm run dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm run build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm run preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

7
frontend/app.vue Normal file
View File

@ -0,0 +1,7 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NavBar />
<NuxtPage />
</div>
</template>

View File

@ -0,0 +1,43 @@
<!--
This component is used to show a loading spinner when the SPA is loading.
Source: https://github.com/barelyhuman/snips/blob/dev/pages/css-loader.md
-->
<div class="loader"></div>
<style>
.loader {
display: block;
position: fixed;
z-index: 1031;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #000;
border-left-color: #000;
border-bottom-color: #efefef;
border-right-color: #efefef;
border-radius: 50%;
-webkit-animation: loader 400ms linear infinite;
animation: loader 400ms linear infinite;
}
\@-webkit-keyframes loader {
0% {
-webkit-transform: translate(-50%, -50%) rotate(0deg);
}
100% {
-webkit-transform: translate(-50%, -50%) rotate(360deg);
}
}
\@keyframes loader {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
</style>

BIN
frontend/bun.lockb Executable file

Binary file not shown.

View File

@ -0,0 +1,22 @@
<template>
<div class="avatar">
<div class="rounded-full">
<img :src="getAvatarUrl(user)" />
</div>
</div>
</template>
<script setup lang="ts">
import type { User } from '~/types/user';
const props = defineProps<{
user: User,
}>();
const getAvatarUrl = (user: User) => {
if (user.avatar) {
return user.avatar;
}
return 'https://avatar.iran.liara.run/username?username=' + user.name;
};
</script>

View File

@ -0,0 +1,71 @@
<template>
<dialog
id="login_modal"
ref="login_modal"
@cancel.prevent=""
@keyup="handleKeyPress"
class="modal"
>
<div class="modal-box">
<h3 class="text-3xl text-center mb-6">Connexion</h3>
<div class="flex flex-wrap gap-5 justify-center">
<template v-for="(user, index) in users" :key="user.id">
<LoginModalAvatar
:user="user"
:rank="index+1"
@selectUser="login"
/>
</template>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button class="cursor-default">close</button>
</form>
</dialog>
</template>
<script setup lang="ts">
import type { User } from '~/types/user';
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://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');
const current_user = useCurrentUser();
const login = (user: User) => {
console.log('login', user);
current_user.value = user;
loginModal.value?.close();
};
const handleKeyPress = (event: KeyboardEvent) => {
// Extract the rank from the event.code : Digit7 -> 7
const rank = event.code.match(/\d/);
if (!rank) {
console.debug('Not handled key event', { event });
return;
}
const user = getUserByRank(parseInt(rank[0]));
if (user) {
login(user);
} else {
console.debug('Not handled key event', { event });
}
};
const getUserByRank = (rank: number): User => {
return users[rank - 1];
};
</script>

View File

@ -0,0 +1,17 @@
<template>
<button class="relative" @click="$emit('selectUser', user)">
<Avatar class="w-24" :user="user" />
<div class="absolute w-fit mx-auto bottom-0 inset-x-0">
<kbd class="kbd kbd-sm">{{ rank }}</kbd>
</div>
</button>
</template>
<script setup lang="ts">
import type { User } from '~/types/user';
const props = defineProps<{
user: User,
rank: Number,
}>();
</script>

View File

@ -0,0 +1,35 @@
<template>
<div class="navbar">
<div class="navbar-start">
<a class="btn btn-ghost text-xl" href="/">Chrys4lide</a>
</div>
<nav class="navbar-center">
<NuxtLink to="/" class="btn btn-ghost">Accueil</NuxtLink>
<NuxtLink to="/CPS" class="btn btn-ghost">Carte CPS</NuxtLink>
</nav>
<div class="navbar-end">
<template v-if="!current_user">
<button class="btn btn-ghost" type="button" onclick="login_modal.showModal()">
Connexion
</button>
</template>
<template v-else>
<details class="dropdown dropdown-end">
<summary class="block"><Avatar :user="current_user" class="w-12" role="button" /></summary>
<ul class="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
<li><a @click="logout">Déconnexion</a></li>
</ul>
</details>
</template>
</div>
</div>
<LoginModal />
</template>
<script setup lang="ts">
const current_user = useCurrentUser();
const logout = () => {
current_user.value = null;
};
</script>

View File

@ -0,0 +1,3 @@
import type { User } from '@/types/user';
export const useCurrentUser = () => useState<User | null>('currentUser', () => null);

39
frontend/nuxt.config.ts Normal file
View File

@ -0,0 +1,39 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
// Enables the development server to be discoverable by other devices for mobile development
devServer: { host: '0.0.0.0', port: 1420 },
devtools: { enabled: true },
modules: [
'@nuxtjs/tailwindcss',
'@nuxtjs/color-mode',
],
// Disable SSR for Tauri
ssr: false,
vite: {
// Better support for Tauri CLI output
clearScreen: false,
// Enable environment variables
// Additional environment variables can be found at
// https://v2.tauri.app/reference/environment-variables/
envPrefix: ['VITE_', 'TAURI_'],
server: {
// Tauri requires a consistent port
strictPort: true,
hmr: {
// Use websocket for mobile hot reloading
protocol: 'ws',
// Make sure it's available on the network
host: '0.0.0.0',
// Use a specific port for hmr
port: 5183,
},
},
},
colorMode: {
// Add `data-theme` attribute to the `html` tag, allowing DaisyUI to handle dark mode automatically
dataValue: 'theme',
// Remove the default `-mode` suffix from the class name, letting have `dark` and `light` as class names, for DaisyUI compatibility
classSuffix: '',
},
})

22
frontend/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxi build",
"dev": "nuxi dev",
"generate": "nuxi generate",
"preview": "nuxi preview",
"postinstall": "nuxi prepare"
},
"dependencies": {
"@nuxtjs/color-mode": "^3.5.1",
"daisyui": "^4.12.10",
"nuxt": "^3.13.0",
"vue": "latest",
"vue-router": "latest"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.12.1"
}
}

8
frontend/pages/CPS.vue Normal file
View File

@ -0,0 +1,8 @@
<template>
<div>
<h1 class="text-xl">Carte CPS</h1>
</div>
</template>
<script setup>
</script>

17
frontend/pages/index.vue Normal file
View File

@ -0,0 +1,17 @@
<template>
<div>
<h1 class="text-xl">Welcome to your {{ appName }}!</h1>
<p v-if="current_user">Logged in as {{ current_user.name }}</p>
</div>
</template>
<script setup lang="ts">
const current_user = useCurrentUser();
const appName = 'Nuxt App';
</script>
<style scoped>
h1 {
color: #42b983;
}
</style>

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

View File

@ -0,0 +1,7 @@
import type { Config } from 'tailwindcss'
export default <Partial<Config>>{
plugins: [
require('daisyui'),
],
}

4
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

6
frontend/types/user.ts Normal file
View File

@ -0,0 +1,6 @@
export declare interface User {
id: number;
name: string;
email?: string;
avatar?: string;
}