lab_presence

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

main.rs (7594B)


      1 use pcsc::{Context, Protocols, Scope, ShareMode};
      2 use shared::{MemberDirectory, UserId};
      3 use std::thread::sleep;
      4 use std::time::Duration;
      5 
      6 struct NfcSender {
      7     core_url: String,
      8     token: String,
      9     member_directory: MemberDirectory,
     10 }
     11 
     12 impl NfcSender {
     13     fn new() -> Self {
     14         Self {
     15             core_url: String::new(),
     16             token: String::new(),
     17             member_directory: MemberDirectory::new(vec![]),
     18         }
     19     }
     20     fn init(&mut self) -> Result<(), Box<dyn std::error::Error>> {
     21         self.core_url =
     22             std::env::var("CORE_URL").unwrap_or_else(|_| "https://localhost:8443".to_string());
     23         self.member_directory = serde_json::from_str(&std::env::var("MEMBER_DIRECTORY")?)?;
     24         let allowed_devices: serde_json::Value =
     25             serde_json::from_str(&std::env::var("ALLOWED_DEVICES")?)?;
     26         let secret: &str = allowed_devices["nfc"]
     27             .as_str()
     28             .ok_or("ALLOWED_DEVICES.nfc is not a string or missing")?;
     29         self.login(secret.to_string())?;
     30         Ok(())
     31     }
     32 
     33     fn login(&mut self, secret: String) -> Result<(), Box<dyn std::error::Error>> {
     34         let login_request = shared::LoginRequest {
     35             device_id: "nfc".to_string(),
     36             device_secret: secret,
     37         };
     38         let response: shared::LoginResponse =
     39             ureq::post(&format!("{}/api/v1/login", self.core_url))
     40                 .send_json(login_request)?
     41                 .body_mut()
     42                 .read_json::<shared::LoginResponse>()?;
     43         self.token = response.access_token;
     44         Ok(())
     45     }
     46     fn update_presence(&self, user_id: &UserId, presence: &str) -> Result<(), ureq::Error> {
     47         let request_body = shared::PresenceUpdateRequest {
     48             presence: presence.to_string(),
     49             status_msg: "NFC toggle request sent".to_string(),
     50         };
     51         let _ = ureq::put(&format!(
     52             "{}/api/v1/presence/{}/status",
     53             self.core_url, user_id.id
     54         ))
     55         .header("Authorization", &format!("Bearer {}", self.token))
     56         .send_json(request_body)?;
     57         Ok(())
     58     }
     59     fn get_presence(&self, user_id: &UserId) -> Result<String, ureq::Error> {
     60         let now_presence_res = ureq::get(&format!(
     61             "{}/api/v1/presence/{}/status",
     62             self.core_url, user_id.id
     63         ))
     64         .header("Authorization", &format!("Bearer {}", self.token))
     65         .call()?
     66         .body_mut()
     67         .read_json::<shared::PresenceResponse>()?;
     68         Ok(now_presence_res.presence)
     69     }
     70     fn toggled_presence(presence: &str) -> &'static str {
     71         if presence == "inLab" {
     72             "offline"
     73         } else {
     74             "inLab"
     75         }
     76     }
     77 }
     78 fn main() -> Result<(), Box<dyn std::error::Error>> {
     79     let _ = dotenvy::dotenv();
     80     let ctx = Context::establish(Scope::User)?;
     81 
     82     let mut nfc_sender: NfcSender = NfcSender::new();
     83     nfc_sender.init()?;
     84 
     85     println!("NFC sender has started...");
     86 
     87     loop {
     88         let mut readers_buf = [0u8; 2048];
     89         let mut readers = match ctx.list_readers(&mut readers_buf) {
     90             Ok(r) => r,
     91             Err(_) => {
     92                 sleep(Duration::from_secs(1));
     93                 continue;
     94             }
     95         };
     96 
     97         let reader_name = match readers.next() {
     98             Some(name) => name,
     99             None => {
    100                 sleep(Duration::from_secs(1));
    101                 continue;
    102             }
    103         };
    104 
    105         if let Ok(card) = ctx.connect(reader_name, ShareMode::Shared, Protocols::ANY) {
    106             let felica_poll_cmd = [
    107                 0xFF, 0x00, 0x00, 0x00, 0x08, 0xD4, 0x40, 0x01, 0x00, 0xFF, 0xFF, 0x01, 0x00,
    108             ];
    109             let mut rapdu_buf = [0u8; 258];
    110 
    111             if let Ok(rapdu) = card.transmit(&felica_poll_cmd, &mut rapdu_buf) {
    112                 if rapdu.len() >= 18 {
    113                     let idm_bytes = &rapdu[10..18];
    114                     let idm_str = idm_bytes
    115                         .iter()
    116                         .map(|b| format!("{:02X}", b))
    117                         .collect::<String>();
    118 
    119                     if let Ok(user_id) = nfc_sender.member_directory.inquire(&idm_str) {
    120                         if let Ok(current_presence) = nfc_sender.get_presence(&user_id) {
    121                             let toggled = NfcSender::toggled_presence(&current_presence);
    122                             let _ = nfc_sender.update_presence(&user_id, toggled);
    123                         }
    124                     }
    125 
    126                     sleep(Duration::from_secs(2));
    127                 }
    128             }
    129         }
    130 
    131         sleep(Duration::from_millis(500));
    132     }
    133 }
    134 #[cfg(test)]
    135 mod tests {
    136     use super::*;
    137 
    138     fn extract_idm(rapdu: &[u8]) -> Option<String> {
    139         if rapdu.len() < 18 {
    140             return None;
    141         }
    142         Some(
    143             rapdu[10..18]
    144                 .iter()
    145                 .map(|b| format!("{:02X}", b))
    146                 .collect::<String>(),
    147         )
    148     }
    149 
    150     #[test]
    151     fn test_toggled_presence_in_lab_to_offline() {
    152         let current = "inLab";
    153         let next = NfcSender::toggled_presence(current);
    154         assert_eq!(next, "offline");
    155     }
    156 
    157     #[test]
    158     fn test_toggled_presence_offline_to_in_lab() {
    159         let current = "offline";
    160         let next = NfcSender::toggled_presence(current);
    161         assert_eq!(next, "inLab");
    162     }
    163 
    164     #[test]
    165     fn test_toggled_presence_unknown_status_defaults_to_in_lab() {
    166         let current = "unknown";
    167         let next = NfcSender::toggled_presence(current);
    168         assert_eq!(next, "inLab");
    169     }
    170 
    171     #[test]
    172     fn test_extract_idm_valid_rapdu() {
    173         let mut rapdu = vec![0u8; 18];
    174         rapdu[10..18].copy_from_slice(&[0x01, 0x2E, 0x3F, 0x4A, 0x5B, 0x6C, 0x7D, 0x8E]);
    175 
    176         let idm = extract_idm(&rapdu);
    177         assert_eq!(idm, Some("012E3F4A5B6C7D8E".to_string()));
    178     }
    179 
    180     #[test]
    181     fn test_extract_idm_short_rapdu_returns_none() {
    182         let rapdu = vec![0u8; 17];
    183         let idm = extract_idm(&rapdu);
    184         assert_eq!(idm, None);
    185     }
    186 }
    187 #[cfg(test)]
    188 mod integration_tests {
    189     use super::*;
    190     use wiremock::matchers::{header, method, path};
    191     use wiremock::{Mock, MockServer, ResponseTemplate};
    192 
    193     #[tokio::test]
    194     async fn test_nfc_sender_api_flow() -> Result<(), Box<dyn std::error::Error>> {
    195         let mock_server = MockServer::start().await;
    196 
    197         Mock::given(method("GET"))
    198             .and(path("/api/v1/presence/user_123/status"))
    199             .and(header("Authorization", "Bearer mock_token"))
    200             .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
    201                 "presence": "inLab",
    202                 "status_msg": "NFC toggle request sent",
    203                 "timestamp": 1625097600
    204             })))
    205             .mount(&mock_server)
    206             .await;
    207 
    208         Mock::given(method("PUT"))
    209             .and(path("/api/v1/presence/user_123/status"))
    210             .and(header("Authorization", "Bearer mock_token"))
    211             .respond_with(ResponseTemplate::new(200))
    212             .mount(&mock_server)
    213             .await;
    214 
    215         let sender = NfcSender {
    216             core_url: mock_server.uri(),
    217             token: "mock_token".to_string(),
    218             member_directory: MemberDirectory::new(vec![]),
    219         };
    220 
    221         let dummy_user = UserId {
    222             id: "user_123".to_string(),
    223         };
    224 
    225         let presence = sender.get_presence(&dummy_user)?;
    226         assert_eq!(presence, "inLab");
    227 
    228         let toggled = NfcSender::toggled_presence(&presence);
    229         let update_res = sender.update_presence(&dummy_user, toggled);
    230         assert!(update_res.is_ok());
    231 
    232         Ok(())
    233     }
    234 }