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 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 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#[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
130pub 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}