use crate::DbConn; use crate::user; use crate::item; use rocket::http::Status; use rocket::response::status::Custom; use rocket_contrib::json::Json; use serde::{Serialize, Deserialize}; use std::vec::Vec; pub fn routes() -> impl Into> { routes![ auth, auth_change_pw, auth_sign_in, auth_params, auth_ping, items_sync ] } #[derive(Serialize)] #[serde(untagged)] enum Response { Error { errors: Vec }, Success(T) } // Some shorthands type JsonResp = Json>; fn success_resp(resp: T) -> Custom> { Custom(Status::Ok, Json(Response::Success(resp))) } fn error_resp(status: Status, errors: Vec) -> Custom> { Custom(status, Json(Response::Error { errors })) } #[derive(Serialize)] struct AuthResultUser { email: String, uuid: String } #[derive(Serialize)] struct AuthResult { user: AuthResultUser, token: String } #[post("/auth", format = "json", data = "")] fn auth(db: DbConn, new_user: Json) -> Custom> { match user::User::create(&db.0, &new_user) { Ok(_) => _sign_in(db, &new_user.email, &new_user.password), Err(user::UserOpError(e)) => error_resp(Status::InternalServerError, vec![e]) } } #[derive(Deserialize)] struct SignInParams { email: String, password: String } #[post("/auth/sign_in", format = "json", data = "")] fn auth_sign_in(db: DbConn, params: Json) -> Custom> { _sign_in(db, ¶ms.email, ¶ms.password) } // Shared logic for all interfaces that needs to do an automatic sign-in fn _sign_in(db: DbConn, mail: &str, passwd: &str) -> Custom> { // Try to find the user first let res = user::User::find_user_by_email(&db, mail) .and_then(|u| u.create_token(passwd) .map(|x| (u.uuid, u.email, x))); match res { Ok((uuid, email, token)) => success_resp(AuthResult { user: AuthResultUser { uuid, email }, token }), Err(user::UserOpError(e)) => error_resp(Status::InternalServerError, vec![e]) } } #[derive(Serialize)] struct AuthParams { pw_cost: i32, pw_nonce: String, version: String } impl Into for user::User { fn into(self) -> AuthParams { AuthParams { pw_cost: self.pw_cost, pw_nonce: self.pw_nonce, version: self.version } } } #[get("/auth/params?")] fn auth_params(db: DbConn, email: String) -> Custom> { match user::User::find_user_by_email(&db, &email) { Ok(u) => success_resp(u.into()), Err(user::UserOpError(e)) => error_resp(Status::InternalServerError, vec![e]) } } #[derive(Deserialize)] struct ChangePwParams { email: String, password: String, current_password: String } #[post("/auth/change_pw", format = "json", data = "")] fn auth_change_pw(db: DbConn, params: Json) -> Custom> { let res = user::User::find_user_by_email(&db, ¶ms.email) .and_then(|u| u.change_pw(&db, ¶ms.current_password, ¶ms.password)); match res { Ok(_) => Custom(Status::NoContent, Json(Response::Success(()))), Err(user::UserOpError(e)) => error_resp(Status::InternalServerError, vec![e]) } } // For testing the User request guard #[get("/auth/ping")] fn auth_ping(_db: DbConn, u: user::User) -> Custom> { Custom(Status::Ok, Json(Response::Success(u.email))) } #[derive(Deserialize)] struct SyncParams { items: Vec, sync_token: Option, cursor_token: Option, limit: Option } #[derive(Serialize)] struct SyncResp { retrieved_items: Vec, saved_items: Vec, unsaved: Vec, sync_token: Option, // for convenience, we will actually always return this cursor_token: Option } #[post("/items/sync", format = "json", data = "")] fn items_sync(db: DbConn, u: user::User, params: Json) -> Custom> { let mut resp = SyncResp { retrieved_items: vec![], saved_items: vec![], unsaved: vec![], sync_token: params.sync_token.clone(), cursor_token: params.cursor_token.clone() }; let inner_params = params.into_inner(); // First, update all items sent by client for it in inner_params.items.into_iter() { if let Err(item::ItemOpError(_)) = item::SyncItem::items_insert(&db, &u, &it) { // Well, we should try twice... // TODO: make this more elegant (also handle differneces between db error and conflict) // (if we were ever to implement a conflict feature) if let Err(item::ItemOpError(_)) = item::SyncItem::items_insert(&db, &u, &it) { // Let's not fail just because one of them... // At least the client will know there's an error // (maybe mistakes it for conflict) resp.unsaved.push(it); } } else { resp.saved_items.push(it); } } let mut from_id: Option = None; let mut max_id: Option = None; if let Some(cursor_token) = inner_params.cursor_token { // If the client provides cursor_token, // then, we return all records // until sync_token (the head of the last sync) from_id = cursor_token.parse().ok(); max_id = inner_params.sync_token.clone() .and_then(|i| i.parse().ok()); } else if let Some(sync_token) = inner_params.sync_token { // If there is no cursor_token, then we are doing // a normal sync, so just return all records from sync_token from_id = sync_token.parse().ok(); } // Then, retrieve what the client needs let result = item::SyncItem::items_of_user(&db, &u, from_id, max_id, inner_params.limit); match result { Err(item::ItemOpError(e)) => { error_resp(Status::InternalServerError, vec![e]) }, Ok(items) => { if !items.is_empty() { // max_id = the last sync token // if we still haven't reached the last sync token yet, // return a new cursor token and keep the sync token if let Some(max_id) = max_id { resp.cursor_token = Some(items[0].id.to_string()); resp.sync_token = Some(max_id.to_string()); } else { // Else, use the current max id as the sync_token resp.sync_token = Some(items[0].id.to_string()); } } resp.retrieved_items = items.into_iter().map(|x| x.into()).collect(); Custom(Status::Ok, Json(Response::Success(resp))) } } }