lab_presence

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

main.rs (7982B)


      1 mod auth;
      2 use auth::{AuthenticatedDevice, generate_secure_token, validate_device};
      3 
      4 use actix_web::{
      5     App, HttpRequest, HttpResponse, HttpServer, Responder, get, http::header::ContentType,
      6     middleware, post, put, web,
      7 };
      8 use log::debug;
      9 use rcgen::generate_simple_self_signed;
     10 use rustls::{
     11     ServerConfig,
     12     pki_types::{CertificateDer, PrivateKeyDer},
     13 };
     14 use shared::*;
     15 use std::sync::Mutex;
     16 use std::{collections::HashMap, time::UNIX_EPOCH};
     17 
     18 struct AppState {
     19     presences: Mutex<HashMap<String, LabPresence>>,
     20     tokens: Mutex<HashMap<String, String>>,
     21 }
     22 
     23 /// simple handle
     24 async fn index(req: HttpRequest) -> HttpResponse {
     25     debug!("{req:?}");
     26 
     27     HttpResponse::Ok().content_type(ContentType::html()).body(
     28         "<!DOCTYPE html><html><body>\
     29             <h1>Lab Presence</h1>\
     30         </body></html>",
     31     )
     32 }
     33 
     34 #[actix_web::main]
     35 async fn main() -> std::io::Result<()> {
     36     env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
     37 
     38     let config = load_rustls_config();
     39 
     40     let state = web::Data::new(AppState {
     41         presences: Mutex::new(HashMap::new()),
     42         tokens: Mutex::new(HashMap::new()),
     43     });
     44 
     45     log::info!("starting HTTPS server at https://localhost:8443");
     46 
     47     HttpServer::new(move || {
     48         App::new()
     49             .app_data(state.clone())
     50             .wrap(middleware::Logger::default())
     51             .service(login)
     52             .service(get_presence)
     53             .service(put_presence)
     54             .service(web::resource("/index.html").to(index))
     55             .service(web::redirect("/", "/index.html"))
     56     })
     57     .bind_rustls_0_23("127.0.0.1:8443", config)?
     58     .run()
     59     .await
     60 }
     61 
     62 fn load_rustls_config() -> rustls::ServerConfig {
     63     rustls::crypto::aws_lc_rs::default_provider()
     64         .install_default()
     65         .unwrap();
     66 
     67     let subject_alt_names = vec!["localhost".to_string(), "127.0.0.1".to_string()];
     68     let cert = generate_simple_self_signed(subject_alt_names).unwrap();
     69 
     70     let cert_chain = vec![CertificateDer::from(cert.cert.der().to_vec())];
     71     let key_der = PrivateKeyDer::Pkcs8(cert.signing_key.serialize_der().into());
     72 
     73     ServerConfig::builder()
     74         .with_no_client_auth()
     75         .with_single_cert(cert_chain, key_der)
     76         .unwrap()
     77 }
     78 
     79 #[post("/api/v1/login")]
     80 async fn login(data: web::Json<LoginRequest>, state: web::Data<AppState>) -> impl Responder {
     81     if !validate_device(&data.device_id, &data.device_secret) {
     82         return HttpResponse::Forbidden().json(ErrorResponse {
     83             errcode: "M_FORBIDDEN".to_string(),
     84             error: "authorization failed".to_string(),
     85         });
     86     }
     87 
     88     let token = generate_secure_token();
     89     let mut tokens = state.tokens.lock().unwrap();
     90     tokens.insert(token.clone(), data.device_id.clone());
     91 
     92     HttpResponse::Ok().json(LoginResponse {
     93         access_token: token,
     94         expires_in: 3600,
     95     })
     96 }
     97 
     98 #[get("/api/v1/presence/{userId}/status")]
     99 async fn get_presence(
    100     path: web::Path<String>,
    101     _auth: AuthenticatedDevice,
    102     state: web::Data<AppState>,
    103 ) -> impl Responder {
    104     let user_id = path.into_inner();
    105     let presences = state.presences.lock().unwrap();
    106 
    107     if let Some(presence) = presences.get(&user_id) {
    108         HttpResponse::Ok().json(PresenceResponse {
    109             presence: presence.presence().to_string(),
    110             status_msg: presence.status_msg().unwrap_or("").to_string(),
    111             timestamp: presence.timestamp(),
    112         })
    113     } else {
    114         HttpResponse::NotFound().json(ErrorResponse {
    115             errcode: "M_UNKNOWN".to_string(),
    116             error: "An unknown error occurred".to_string(),
    117         })
    118     }
    119 }
    120 
    121 #[put("/api/v1/presence/{userId}/status")]
    122 async fn put_presence(
    123     path: web::Path<String>,
    124     data: web::Json<PresenceUpdateRequest>,
    125     _auth: AuthenticatedDevice,
    126     state: web::Data<AppState>,
    127 ) -> impl Responder {
    128     let user_id = path.into_inner();
    129     let mut presences = state.presences.lock().unwrap();
    130 
    131     let status_msg = if data.status_msg.is_empty() {
    132         None
    133     } else {
    134         Some(data.status_msg.clone())
    135     };
    136 
    137     if let Some(existing) = presences.get_mut(&user_id) {
    138         match existing.update(
    139             data.presence.clone(),
    140             status_msg,
    141             std::time::SystemTime::now()
    142                 .duration_since(UNIX_EPOCH)
    143                 .unwrap()
    144                 .as_secs() as u64,
    145         ) {
    146             Ok(_) => HttpResponse::Ok().json(serde_json::json!({})),
    147             Err(e) => HttpResponse::BadRequest().json(ErrorResponse {
    148                 errcode: "M_BAD_JSON".to_string(),
    149                 error: e.to_string(),
    150             }),
    151         }
    152     } else {
    153         presences.insert(
    154             user_id,
    155             LabPresence::new(
    156                 data.presence.clone(),
    157                 status_msg,
    158                 std::time::SystemTime::now()
    159                     .duration_since(UNIX_EPOCH)
    160                     .unwrap()
    161                     .as_secs() as u64,
    162             ),
    163         );
    164         HttpResponse::Ok().json(serde_json::json!({}))
    165     }
    166 }
    167 
    168 #[cfg(test)]
    169 mod tests {
    170     use super::*;
    171     use actix_web::{App, test, web};
    172     use shared::{LoginRequest, LoginResponse, PresenceUpdateRequest};
    173 
    174     #[actix_web::test]
    175     async fn test_auth_and_presence_flow() {
    176         let state = web::Data::new(AppState {
    177             presences: Mutex::new(HashMap::new()),
    178             tokens: Mutex::new(HashMap::new()),
    179         });
    180 
    181         let app = test::init_service(
    182             App::new()
    183                 .app_data(state.clone())
    184                 .service(login)
    185                 .service(get_presence)
    186                 .service(put_presence),
    187         )
    188         .await;
    189 
    190         let invalid_login_req = test::TestRequest::post()
    191             .uri("/api/v1/login")
    192             .set_json(&LoginRequest {
    193                 device_id: "nfc_reader_01".to_string(),
    194                 device_secret: "wrong_secret".to_string(),
    195             })
    196             .to_request();
    197 
    198         let resp = test::call_service(&app, invalid_login_req).await;
    199         assert_eq!(resp.status(), 403);
    200 
    201         let valid_login_req = test::TestRequest::post()
    202             .uri("/api/v1/login")
    203             .set_json(&LoginRequest {
    204                 device_id: "nfc".to_string(),
    205                 device_secret: "nfc".to_string(),
    206             })
    207             .to_request();
    208 
    209         let resp = test::call_service(&app, valid_login_req).await;
    210         assert_eq!(resp.status(), 200);
    211 
    212         let login_res: LoginResponse = test::read_body_json(resp).await;
    213         let token = login_res.access_token;
    214         assert!(!token.is_empty());
    215 
    216         let unauth_put_req = test::TestRequest::put()
    217             .uri("/api/v1/presence/user_hash_123/status")
    218             .set_json(&PresenceUpdateRequest {
    219                 presence: "online".to_string(),
    220                 status_msg: "In Lab".to_string(),
    221             })
    222             .to_request();
    223 
    224         let resp = test::call_service(&app, unauth_put_req).await;
    225         assert_eq!(resp.status(), 403);
    226 
    227         let auth_put_req = test::TestRequest::put()
    228             .uri("/api/v1/presence/user_hash_123/status")
    229             .insert_header(("Authorization", format!("Bearer {}", token)))
    230             .set_json(&PresenceUpdateRequest {
    231                 presence: "online".to_string(),
    232                 status_msg: "In Lab".to_string(),
    233             })
    234             .to_request();
    235 
    236         let resp = test::call_service(&app, auth_put_req).await;
    237         assert_eq!(resp.status(), 200);
    238 
    239         let get_req = test::TestRequest::get()
    240             .uri("/api/v1/presence/user_hash_123/status")
    241             .insert_header(("Authorization", format!("Bearer {}", token)))
    242             .to_request();
    243 
    244         let resp = test::call_service(&app, get_req).await;
    245         assert_eq!(resp.status(), 200);
    246 
    247         let presence_res: PresenceResponse = test::read_body_json(resp).await;
    248         assert_eq!(presence_res.presence, "online");
    249         assert_eq!(presence_res.status_msg, "In Lab");
    250     }
    251 }