Compare commits

...

26 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: #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: #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
41 changed files with 1741 additions and 649 deletions

980
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -2,12 +2,14 @@
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`
- `desktop`: Client desktop propulsé par Tauri, encapsulant le serveur web `app`
- `sesam-vitale`: Bibliothèque de gestion des services SESAM-Vitale (Lecture des cartes CPS et Vitale, téléservices ...)
- `utils`: Bibliothèque de fonctions utilitaires
- `crates`: Dossier racine des modules Rust
- `crates/backend`: Serveur backend propulsé par Axum, exposant une API REST
- `crates/desktop`: Client desktop propulsé par Tauri, exposant le `frontend`
- `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
@ -26,57 +28,49 @@ Des exemples de fichiers de configuration sont disponibles à la racine du proje
### 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
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 :
```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
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
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
# 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
```
/!\ 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
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
cargo tauri build

View File

@ -1,4 +1,4 @@
use std::path::Path;
use std::path::PathBuf;
use axum::http::{StatusCode, Uri};
use axum_htmx::AutoVaryLayer;
@ -11,7 +11,7 @@ async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
}
pub fn get_router(assets_path: &Path) -> axum::Router {
pub async fn get_router(assets_path: PathBuf) -> axum::Router<()> {
axum::Router::new()
.nest_service("/assets", ServeDir::new(assets_path))
.merge(pages::get_routes())

View File

@ -72,7 +72,7 @@ async fn main() -> Result<(), AppError> {
let livereload_layer =
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 local_addr = listener.local_addr().map_err(AppError::TCPListener)?;

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"]
[build-dependencies]
tauri-build = { version = "2.0.0-beta", features = [] }
tauri-build = { version = "2.0.0-rc", features = [] }
[dependencies]
axum = "0.7.5"
tauri = { version = "2.0.0-beta", features = [] }
tower = "0.4.13"
tokio = "1.39.1"
app = { path = "../app" }
http = "1.1.0"
bytes = "1.6.1"
thiserror = "1.0.63"
tauri = { version = "2.0.0-rc", features = [] }
tauri-plugin-shell = "2.0.0-rc"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

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};
use axum::Router;
use bytes::Bytes;
use http::{request, response, Request, Response};
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)
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let assets_path: PathBuf = app
.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"),
)
}
}
});
})
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.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",
"identifier": "org.p4pillon.pharma.desktop",
"identifier": "org.p4pillon.chrys4lide.lgo",
"build": {
"beforeDevCommand": {
"cwd": "../app",
"script": "cargo run"
"cwd": "../../frontend",
"script": "bun run dev"
},
"devUrl": "http://localhost:3000",
"frontendDist": "axum://place.holder/"
"devUrl": "http://localhost:1420",
"beforeBuildCommand": {
"cwd": "../../frontend",
"script": "bun run generate"
},
"frontendDist": "../../frontend/dist"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "Logiciel Pharma",
"title": "Chrys4lide | LG0",
"width": 800,
"height": 600
}
@ -25,9 +29,6 @@
},
"bundle": {
"active": true,
"resources": {
"../app/assets/": "./assets/"
},
"targets": "all",
"icon": [
"icons/32x32.png",
@ -37,5 +38,4 @@
"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),
#[error(transparent)]
SSVLibErrorCode(#[from] crate::libssv::LibSSVError),
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
}
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;
}