use chrono::{NaiveDateTime, TimeZone}; use model::{AccessTokenString, RefreshTokenString}; use seed::prelude::*; use crate::shared::notification::NotificationMsg; use crate::{Model, Msg}; #[derive(Debug)] pub enum SessionMsg { SessionReceived { access_token: AccessTokenString, refresh_token: RefreshTokenString, exp: NaiveDateTime, }, SessionExpired, RefreshToken(RefreshTokenString), TokenRefreshed(crate::api::NetRes), CheckSession, } pub fn init(model: &mut Model, orders: &mut impl Orders) { orders.stream(streams::interval(500, || { Msg::Session(SessionMsg::CheckSession) })); model.shared.access_token = LocalStorage::get::<_, String>("at") .ok() .map(model::AccessTokenString::new); model.shared.refresh_token = LocalStorage::get::<_, String>("rt") .ok() .map(model::RefreshTokenString::new); model.shared.exp = LocalStorage::get::<_, String>("exp").ok().and_then(|s| { seed::log!("Parsing ", s); chrono::DateTime::parse_from_rfc3339(&s) .ok() .map(|t| t.naive_utc()) }) } pub fn update(msg: SessionMsg, model: &mut Model, orders: &mut impl Orders) { match msg { SessionMsg::SessionReceived { access_token, refresh_token, exp, } => { LocalStorage::insert("at", access_token.as_str()).ok(); LocalStorage::insert("rt", refresh_token.as_str()).ok(); let encoded = { let l: chrono::DateTime = chrono::Local.from_local_datetime(&exp).unwrap(); l.to_rfc3339() }; LocalStorage::insert("exp", &encoded).ok(); model.shared.access_token = Some(access_token); model.shared.refresh_token = Some(refresh_token); model.shared.exp = Some(exp); } SessionMsg::SessionExpired => { LocalStorage::remove("at").ok(); LocalStorage::remove("rt").ok(); LocalStorage::remove("exp").ok(); orders.force_render_now(); } SessionMsg::CheckSession => { orders.skip(); if model.shared.refresh_token.is_none() { return; } if let Some(exp) = model.shared.exp { if exp > chrono::Utc::now().naive_utc() - chrono::Duration::seconds(1) { return; } if let Some(token) = model.shared.refresh_token.take() { orders .skip() .send_msg(Msg::Session(SessionMsg::RefreshToken(token))); } } } SessionMsg::RefreshToken(token) => { orders.skip().perform_cmd(async { Msg::Session(SessionMsg::TokenRefreshed( crate::api::public::refresh_token(token).await, )) }); } SessionMsg::TokenRefreshed(crate::api::NetRes::Success(model::api::SessionOutput { access_token, refresh_token, exp, })) => { orders .skip() .send_msg(Msg::Session(SessionMsg::SessionReceived { access_token, refresh_token, exp, })); } SessionMsg::TokenRefreshed(crate::api::NetRes::Error(model::api::Failure { errors })) => { errors.into_iter().for_each(|msg| { orders .skip() .send_msg(Msg::Shared(crate::shared::Msg::Notification( NotificationMsg::Error(msg), ))); }); } SessionMsg::TokenRefreshed(crate::api::NetRes::Http(e)) => match e { FetchError::JsonError(json) => { seed::error!("invalid data in response", json); } FetchError::DomException(dom) => { seed::error!("dom", dom); } FetchError::PromiseError(res) => { seed::error!("promise", res); } FetchError::NetworkError(net) => { seed::error!("net", net); } FetchError::RequestError(e) => { seed::log!(e); } FetchError::StatusError(status) => match status.code { 401 | 403 => { orders .skip() .send_msg(Msg::Session(SessionMsg::SessionExpired)); } _ => { orders .skip() .send_msg(Msg::Shared(crate::shared::Msg::Notification( NotificationMsg::Error("Request failed".into()), ))); } }, }, } }