use chrono::Utc; use rocket::config::Config; use rocket::config::Environment; use rocket::config::Value; use rocket::http; use rocket::http::Cookie; use rocket::http::Cookies; use rocket::http::Status; use rocket::request; use rocket::request::Form; use rocket::request::FromForm; use rocket::request::FromRequest; use rocket::request::Request; use rocket::response::Redirect; use rocket::State; use rocket_contrib::serve::StaticFiles; use rocket_contrib::templates::Template; use serde_json::map::Map; use serde_json::to_value; use serde_json::Value as Json; use std::collections::HashMap; use crate::db; use crate::error::Error; use crate::importer; use crate::models; use crate::strava; use crate::Params; #[database("db")] pub struct Db(diesel::PgConnection); #[derive(Debug)] pub struct LoggedInUser { pub username: String, } fn default_context() -> Map { let mut data = Map::new(); data.insert("parent".to_string(), json!("layout")); data } impl<'a, 'r> FromRequest<'a, 'r> for LoggedInUser { type Error = Error; fn from_request(request: &'a Request<'r>) -> request::Outcome { let conn = request .guard::() .map_failure(|(s, ())| (s, Error::InternalError))?; let user = (|| { let username = request .cookies() .get_private("user") .map(|cookie| cookie.value().to_string()) .ok_or(Error::NotFound)?; db::get_user(&conn, &username)?; Ok(LoggedInUser { username: username }) })(); debug!("user: {:?}", user); use request::Outcome; match user { Ok(user) => { info!("Credentials: {:?}", user); Outcome::Success(user) } Err(Error::NotFound) => Outcome::Forward(()), Err(e) => Outcome::Failure((Status::InternalServerError, e)), } } } #[get("/p/")] fn profile(conn: Db, username: String) -> Result { let mut context = default_context(); context.insert("title".to_string(), json!(username)); let entries = db::get_entries(&*conn, &username, "run")?; let template = db::get_template(&*conn, "run")?; context.insert("document".to_string(), template.apply(entries)?); Ok(Template::render("profile", context)) } #[get("/", rank = 1)] fn index_logged_in(user: LoggedInUser) -> Redirect { Redirect::to(uri!(profile: user.username)) } #[get("/", rank = 2)] fn index() -> Result { let mut context = default_context(); context.insert("message".to_string(), json!("Hello, World")); Ok(Template::render("index", context)) } #[get("/login?")] fn login(failed: bool) -> Template { let mut context = default_context(); if failed { context.insert( "message".to_string(), json!("Incorrect username or password"), ); } Template::render("login", context) } #[derive(FromForm)] struct LoginData { username: String, password: String, } #[post("/login", data = "")] fn login_submit(conn: Db, data: Form, mut cookies: Cookies) -> Result { match db::authenticate(&*conn, &data.username, &data.password) { Ok(_user) => { let mut cookie = Cookie::new("user", data.username.clone()); cookie.set_same_site(http::SameSite::Lax); cookies.add_private(cookie); Ok(Redirect::to(uri!(index).to_string())) } Err(Error::NotFound) => Ok(Redirect::to(uri!(login: failed = true).to_string())), Err(e) => Err(e), } } #[get("/link_strava_callback?")] fn link_strava_callback( conn: Db, user: LoggedInUser, params: State, code: String, ) -> Result { let token = strava::exchange_token( ¶ms.strava_client_id, ¶ms.strava_client_secret, &code, )?; let result = format!("{:#?}", token); db::insert_strava_token( &*conn, &models::StravaToken { username: user.username, refresh_token: token.refresh_token, access_token: token.access_token, expires_at: token.expires_at, }, )?; Ok(result) } #[get("/import_strava")] fn import_strava(conn: Db, user: LoggedInUser) -> Result<(), Error> { let user = db::get_user(&*conn, &user.username)?; let command = importer::Command::ImportStravaUser { username: user.username.clone(), }; db::insert_task( &conn, &models::NewTask { start_at: Utc::now(), state: models::TaskState::NEW, username: user.username.as_str(), payload: &to_value(command)?, }, )?; Ok(()) } #[get("/link_strava")] fn link_strava(params: State) -> Redirect { Redirect::to(format!( concat!( "https://www.strava.com/oauth/authorize?", "client_id={}&", "response_type=code&", "redirect_uri={}&", "approval_prompt=force&", "scope=read_all,activity:read_all,profile:read_all", ), params.strava_client_id, format!("{}/link_strava_callback", params.base_url) )) } pub fn start(conn: diesel::PgConnection, db_url: &str, base_url: &str, static_path: &str, port: u16, template_path: &str) { let mut database_config = HashMap::new(); let mut databases = HashMap::new(); database_config.insert("url", Value::from(db_url)); databases.insert("db", Value::from(database_config)); let persistent_config = db::get_config(&conn).expect("loading config"); let params = Params { base_url: base_url.to_string(), strava_client_id: persistent_config.strava_client_id, strava_client_secret: persistent_config.strava_client_secret, }; let config = Config::build(Environment::Development) .extra("databases", databases) .extra("template_dir", template_path) .secret_key(persistent_config.rocket_secret_key) .port(port) .finalize() .unwrap(); let strava = strava::StravaImpl::new( params.strava_client_id.clone(), params.strava_client_secret.clone(), ); let importer = importer::Importer::new(conn, strava); importer.run(); rocket::custom(config) .manage(params) .mount( "/", routes![ index, index_logged_in, login, import_strava, profile, login_submit, link_strava, link_strava_callback ], ) .mount("/static", StaticFiles::from(static_path)) .attach(Template::fairing()) .attach(Db::fairing()) .launch(); importer.join(); }