use std::collections::HashMap;
use indexmap::indexmap;
use rocket::form::{Context, Contextual, Form, FromForm};
use rocket::http::{Cookie, CookieJar};
use rocket::response::Redirect;
use rocket::serde::{Deserialize, Serialize};
use rocket_dyn_templates::Template;
use printnanny_api_client::models;
use printnanny_services::config::PrintNannyConfig;
use printnanny_services::error::ServiceError;
use printnanny_services::printnanny_api::ApiService;
use super::response::Response;
pub const COOKIE_USER: &str = "printnanny_user";
pub async fn try_device_setup(config: PrintNannyConfig) -> Result<(), ServiceError> {
match config.api.bearer_access_token {
Some(_) => {
let mut service = ApiService::new(config)?;
service.device_setup().await?;
Ok(())
}
None => Err(ServiceError::SetupIncomplete {
field: "api.bearer_access_token".to_string(),
detail: Some("try_device_setup failed, api credentials are not set".to_string()),
}),
}
}
pub async fn is_auth_valid(jar: &CookieJar<'_>) -> Result<Option<PrintNannyConfig>, ServiceError> {
let cookie = jar.get_private(COOKIE_USER);
match cookie {
Some(user_json) => {
let browser_user: models::User = serde_json::from_str(user_json.value())?;
let config = PrintNannyConfig::new()?;
match &config.device {
Some(device) => match &device.user {
Some(remote_user) => {
if remote_user.id != browser_user.id {
warn!(
"Remote user {:?} did not match COOKIE_USER {:?}, deleting cookie to force re-auth",
&remote_user, &browser_user
);
jar.remove_private(Cookie::named(COOKIE_USER));
Ok(None)
} else {
info!("Auth success! COOKIE_USER matches config.device.user");
Ok(Some(config))
}
}
None => Err(ServiceError::SetupIncomplete {
field: "device.user".to_string(),
detail: Some(
"Failed to read device.user from PrintNannyConfig".to_string(),
),
}),
},
None => {
try_device_setup(config).await?;
let config = PrintNannyConfig::new()?;
Ok(Some(config))
}
}
}
None => Ok(None),
}
}
#[derive(Debug, FromForm, Serialize, Deserialize)]
pub struct EmailForm<'v> {
#[field(validate = contains('@').or_else(msg!("invalid email address")))]
email: &'v str,
#[field(validate = eq(true).or_else(msg!("Please agree to submit anonymous debug logs")))]
analytics: bool,
}
#[derive(Debug, FromForm)]
pub struct TokenForm<'v> {
token: &'v str,
}
async fn handle_step1(
form: &EmailForm<'_>,
config: PrintNannyConfig,
) -> Result<Response, Response> {
let service = ApiService::new(config)?;
let res = service.auth_email_create(form.email.to_string()).await;
match res {
Ok(_) => {
let redirect = Redirect::to(format!("/login/{}", form.email));
Ok(Response::Redirect(redirect))
}
Err(e) => {
error!("{}", e);
let mut context = HashMap::new();
context.insert("errors", format!("Something went wrong {:?}", e));
Ok(Response::Template(Template::render("error", context)))
}
}
}
#[post("/", data = "<form>")]
async fn login_step1_submit<'r>(
form: Form<Contextual<'r, EmailForm<'r>>>,
) -> Result<Response, Response> {
info!("Received auth email form response {:?}", form);
let config = PrintNannyConfig::new()?;
match &form.value {
Some(signup) => {
let result = handle_step1(signup, config).await?;
Ok(result)
}
None => {
info!("form.value is empty");
Ok(Response::Template(Template::render(
"authemail",
&form.context,
)))
}
}
}
pub async fn handle_device_update(
config: PrintNannyConfig,
) -> Result<PrintNannyConfig, ServiceError> {
info!("Sending device info using config {:?}", &config);
let mut service = ApiService::new(config)?;
let new_config = service.device_setup().await?;
info!("Success! Config updated: {:?}", &new_config);
Ok(service.config.clone())
}
async fn handle_token_validate(
token: &str,
email: &str,
config: PrintNannyConfig,
) -> Result<PrintNannyConfig, ServiceError> {
let mut auth_config = config.clone();
let service = ApiService::new(config)?;
let res = service.auth_token_validate(email, token).await?;
let bearer_access_token = res.token;
info!("Success! Authenticated and received bearer token");
auth_config.api.bearer_access_token = Some(bearer_access_token);
auth_config.try_save_by_key("api")?;
Ok(auth_config)
}
#[post("/<email>", data = "<form>")]
async fn login_step2_submit<'r>(
email: String,
jar: &CookieJar<'_>,
form: Form<Contextual<'r, TokenForm<'r>>>,
) -> Result<Response, Response> {
info!("Received auth email form response {:?}", form);
let config = PrintNannyConfig::new()?;
match form.value {
Some(ref v) => {
let token = v.token;
let api_config: PrintNannyConfig = handle_token_validate(token, &email, config).await?;
let cookie_value = serde_json::to_string(
&api_config
.device
.expect("Failed to read device")
.user
.expect("Failed to read user"),
)?;
info!(
"Saving COOKIE_USER={} value={}",
&COOKIE_USER, &cookie_value
);
jar.add_private(Cookie::new(COOKIE_USER, cookie_value));
Ok(Response::Redirect(Redirect::to("/")))
}
None => {
info!("form.value is empty");
Ok(Response::Template(Template::render("authemail", config)))
}
}
}
#[get("/<email>")]
fn login_step2(email: String) -> Template {
let mut context = HashMap::new();
context.insert("email", email);
Template::render("authtoken", context)
}
#[get("/?<email>")]
async fn login_step1_email_prepopulated(
email: &str,
jar: &CookieJar<'_>,
) -> Result<Response, Response> {
let get_api_config = jar.get_private(COOKIE_USER);
let mut c = HashMap::new();
c.insert("values", indexmap! {"email" => vec![email]});
c.insert("errors", indexmap! {});
match get_api_config {
Some(_) => Ok(Response::Redirect(Redirect::to("/"))),
None => Ok(Response::Template(Template::render("authemail", c))),
}
}
#[get("/")]
async fn login_step1(jar: &CookieJar<'_>) -> Result<Response, Response> {
let get_api_config = jar.get_private(COOKIE_USER);
match get_api_config {
Some(_) => Ok(Response::Redirect(Redirect::to("/"))),
None => Ok(Response::Template(Template::render(
"authemail",
Context::default(),
))),
}
}
pub fn routes() -> Vec<rocket::Route> {
routes![
login_step1,
login_step1_email_prepopulated,
login_step1_submit,
login_step2,
login_step2_submit,
]
}