printnanny_dash/
auth.rs

1use std::collections::HashMap;
2
3use indexmap::indexmap;
4use rocket::form::{Context, Contextual, Form, FromForm};
5use rocket::http::{Cookie, CookieJar};
6use rocket::response::Redirect;
7use rocket::serde::{Deserialize, Serialize};
8use rocket_dyn_templates::Template;
9
10use printnanny_api_client::models;
11use printnanny_services::config::PrintNannyConfig;
12use printnanny_services::error::ServiceError;
13use printnanny_services::printnanny_api::ApiService;
14
15use super::response::Response;
16pub const COOKIE_USER: &str = "printnanny_user";
17
18pub async fn try_device_setup(config: PrintNannyConfig) -> Result<(), ServiceError> {
19    match config.api.bearer_access_token {
20        Some(_) => {
21            let mut service = ApiService::new(config)?;
22            service.device_setup().await?;
23            Ok(())
24        }
25        None => Err(ServiceError::SetupIncomplete {
26            field: "api.bearer_access_token".to_string(),
27            detail: Some("try_device_setup failed, api credentials are not set".to_string()),
28        }),
29    }
30}
31
32pub async fn is_auth_valid(jar: &CookieJar<'_>) -> Result<Option<PrintNannyConfig>, ServiceError> {
33    let cookie = jar.get_private(COOKIE_USER);
34    match cookie {
35        Some(user_json) => {
36            let browser_user: models::User = serde_json::from_str(user_json.value())?;
37            let config = PrintNannyConfig::new()?;
38
39            // if config + cookie mismatch, nuke cookie and force re-auth
40            match &config.device {
41                Some(device) => match &device.user {
42                    Some(remote_user) => {
43                        if remote_user.id != browser_user.id {
44                            warn!(
45                                "Remote user {:?} did not match COOKIE_USER {:?}, deleting cookie to force re-auth",
46                                &remote_user, &browser_user
47                            );
48                            jar.remove_private(Cookie::named(COOKIE_USER));
49                            // config.try_factory_reset()?;
50                            Ok(None)
51                        } else {
52                            info!("Auth success! COOKIE_USER matches config.device.user");
53                            Ok(Some(config))
54                        }
55                    }
56                    None => Err(ServiceError::SetupIncomplete {
57                        field: "device.user".to_string(),
58                        detail: Some(
59                            "Failed to read device.user from PrintNannyConfig".to_string(),
60                        ),
61                    }),
62                },
63                None => {
64                    try_device_setup(config).await?;
65                    let config = PrintNannyConfig::new()?;
66                    Ok(Some(config))
67                }
68            }
69        }
70        None => Ok(None),
71    }
72}
73
74#[derive(Debug, FromForm, Serialize, Deserialize)]
75pub struct EmailForm<'v> {
76    #[field(validate = contains('@').or_else(msg!("invalid email address")))]
77    email: &'v str,
78    #[field(validate = eq(true).or_else(msg!("Please agree to submit anonymous debug logs")))]
79    analytics: bool,
80}
81
82#[derive(Debug, FromForm)]
83pub struct TokenForm<'v> {
84    token: &'v str,
85}
86
87async fn handle_step1(
88    form: &EmailForm<'_>,
89    config: PrintNannyConfig,
90) -> Result<Response, Response> {
91    let service = ApiService::new(config)?;
92    let res = service.auth_email_create(form.email.to_string()).await;
93    match res {
94        Ok(_) => {
95            let redirect = Redirect::to(format!("/login/{}", form.email));
96            Ok(Response::Redirect(redirect))
97        }
98        Err(e) => {
99            error!("{}", e);
100            let mut context = HashMap::new();
101            context.insert("errors", format!("Something went wrong {:?}", e));
102            Ok(Response::Template(Template::render("error", context)))
103        }
104    }
105}
106// NOTE: We use `Contextual` here because we want to collect all submitted form
107// fields to re-render forms with submitted values on error. If you have no such
108// need, do not use `Contextual`. Use the equivalent of `Form<Submit<'_>>`.
109#[post("/", data = "<form>")]
110async fn login_step1_submit<'r>(
111    form: Form<Contextual<'r, EmailForm<'r>>>,
112) -> Result<Response, Response> {
113    info!("Received auth email form response {:?}", form);
114    let config = PrintNannyConfig::new()?;
115    match &form.value {
116        Some(signup) => {
117            let result = handle_step1(signup, config).await?;
118            Ok(result)
119        }
120        None => {
121            info!("form.value is empty");
122            Ok(Response::Template(Template::render(
123                "authemail",
124                &form.context,
125            )))
126        }
127    }
128}
129
130// NOTE: We use `Contextual` here because we want to collect all submitted form
131// fields to re-render forms with submitted values on error. If you have no such
132// need, do not use `Contextual`. Use the equivalent of `Form<Submit<'_>>`.
133
134pub async fn handle_device_update(
135    config: PrintNannyConfig,
136) -> Result<PrintNannyConfig, ServiceError> {
137    info!("Sending device info using config {:?}", &config);
138    let mut service = ApiService::new(config)?;
139    let new_config = service.device_setup().await?;
140    info!("Success! Config updated: {:?}", &new_config);
141    Ok(service.config.clone())
142}
143
144async fn handle_token_validate(
145    token: &str,
146    email: &str,
147    config: PrintNannyConfig,
148) -> Result<PrintNannyConfig, ServiceError> {
149    let mut auth_config = config.clone();
150    let service = ApiService::new(config)?;
151    let res = service.auth_token_validate(email, token).await?;
152    let bearer_access_token = res.token;
153    info!("Success! Authenticated and received bearer token");
154
155    auth_config.api.bearer_access_token = Some(bearer_access_token);
156    auth_config.try_save_by_key("api")?;
157    Ok(auth_config)
158}
159
160#[post("/<email>", data = "<form>")]
161async fn login_step2_submit<'r>(
162    email: String,
163    jar: &CookieJar<'_>,
164    form: Form<Contextual<'r, TokenForm<'r>>>,
165) -> Result<Response, Response> {
166    info!("Received auth email form response {:?}", form);
167    let config = PrintNannyConfig::new()?;
168    match form.value {
169        Some(ref v) => {
170            let token = v.token;
171            let api_config: PrintNannyConfig = handle_token_validate(token, &email, config).await?;
172            let cookie_value = serde_json::to_string(
173                &api_config
174                    .device
175                    .expect("Failed to read device")
176                    .user
177                    .expect("Failed to read user"),
178            )?;
179            info!(
180                "Saving COOKIE_USER={} value={}",
181                &COOKIE_USER, &cookie_value
182            );
183            jar.add_private(Cookie::new(COOKIE_USER, cookie_value));
184            Ok(Response::Redirect(Redirect::to("/")))
185        }
186        None => {
187            info!("form.value is empty");
188            Ok(Response::Template(Template::render("authemail", config)))
189        }
190    }
191}
192
193#[get("/<email>")]
194fn login_step2(email: String) -> Template {
195    let mut context = HashMap::new();
196    context.insert("email", email);
197    Template::render("authtoken", context)
198}
199
200#[get("/?<email>")]
201async fn login_step1_email_prepopulated(
202    email: &str,
203    jar: &CookieJar<'_>,
204) -> Result<Response, Response> {
205    let get_api_config = jar.get_private(COOKIE_USER);
206    let mut c = HashMap::new();
207    c.insert("values", indexmap! {"email" => vec![email]});
208    c.insert("errors", indexmap! {});
209    match get_api_config {
210        Some(_) => Ok(Response::Redirect(Redirect::to("/"))),
211        None => Ok(Response::Template(Template::render("authemail", c))),
212    }
213}
214
215#[get("/")]
216async fn login_step1(jar: &CookieJar<'_>) -> Result<Response, Response> {
217    let get_api_config = jar.get_private(COOKIE_USER);
218    match get_api_config {
219        Some(_) => Ok(Response::Redirect(Redirect::to("/"))),
220        None => Ok(Response::Template(Template::render(
221            "authemail",
222            Context::default(),
223        ))),
224    }
225}
226
227pub fn routes() -> Vec<rocket::Route> {
228    routes![
229        login_step1,
230        login_step1_email_prepopulated,
231        login_step1_submit,
232        login_step2,
233        login_step2_submit,
234    ]
235}