From f6e45dc89b3b4f9d5ec9e4f6d63073d632ed0d7b Mon Sep 17 00:00:00 2001 From: EETagent Date: Mon, 7 Nov 2022 13:28:23 +0100 Subject: [PATCH 1/4] feat: add admin login request --- api/src/requests.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/src/requests.rs b/api/src/requests.rs index 032af26..088321e 100644 --- a/api/src/requests.rs +++ b/api/src/requests.rs @@ -13,4 +13,12 @@ pub struct LoginRequest { pub struct RegisterRequest { pub application_id: i32, pub personal_id_number: String, +} + + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct AdminLoginRequest { + pub admin_id: i32, + pub password: String, } \ No newline at end of file From d65b92e3be336b696810a724a7709858e6af0ac7 Mon Sep 17 00:00:00 2001 From: EETagent Date: Mon, 7 Nov 2022 13:28:50 +0100 Subject: [PATCH 2/4] feat: move routes inside route dir, require admin auth for candidate create operation --- api/src/lib.rs | 99 +++++++++---------------------------- api/src/routes/admin.rs | 76 ++++++++++++++++++++++++++++ api/src/routes/candidate.rs | 63 +++++++++++++++++++++++ api/src/routes/mod.rs | 2 + 4 files changed, 163 insertions(+), 77 deletions(-) create mode 100644 api/src/routes/admin.rs create mode 100644 api/src/routes/candidate.rs diff --git a/api/src/lib.rs b/api/src/lib.rs index 480e8f9..cd1550d 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1,23 +1,15 @@ #[macro_use] extern crate rocket; -use std::net::SocketAddr; - -use guards::request::auth::{CandidateAuth, AdminAuth}; -use portfolio_core::services::candidate_service::{CandidateService, UserDetails}; -use requests::{LoginRequest, RegisterRequest}; -use rocket::http::Status; -use rocket::{Rocket, Build}; -use rocket::serde::json::Json; use rocket::fairing::{self, AdHoc}; -use rocket::response::status::Custom; -use migration::{MigratorTrait}; -use sea_orm_rocket::{Connection, Database}; +use rocket::{Build, Rocket}; +use migration::MigratorTrait; +use sea_orm_rocket::Database; -mod pool; mod guards; +mod pool; mod requests; mod routes; @@ -26,70 +18,6 @@ use pool::Db; pub use entity::candidate; pub use entity::candidate::Entity as Candidate; -use portfolio_core::crypto::random_8_char_string; - -#[post("/", data = "")] -async fn create(conn: Connection<'_, Db>, post_form: Json) -> Result> { - let db = conn.into_inner(); - let form = post_form.into_inner(); - - let plain_text_password = random_8_char_string(); - - let candidate = CandidateService::create(db, form.application_id, &plain_text_password, form.personal_id_number) - .await; - - if candidate.is_err() { // TODO cleanup - let e = candidate.err().unwrap(); - return Err(Custom(Status::from_code(e.code()).unwrap_or_default(), e.message())); - } - - Ok(plain_text_password) -} - -#[get("/whoami")] -async fn validate(session: CandidateAuth) -> Result> { - let candidate: entity::candidate::Model = session.into(); - Ok(candidate.application.to_string()) -} - -#[get("/admin")] -async fn admin(session: AdminAuth) -> Result> { - Ok("Hello admin".to_string()) -} - -#[put("/details", data = "
")] -async fn fill_details(conn: Connection<'_, Db>, details: Json, session: CandidateAuth) -> Result> { - let db = conn.into_inner(); - let form = details.into_inner(); - let candidate: entity::candidate::Model = session.into(); - - let candidate = CandidateService::add_user_details(db, candidate, form) - .await; - - if candidate.is_err() { // TODO cleanup - let e = candidate.err().unwrap(); - return Err(Custom(Status::from_code(e.code()).unwrap_or_default(), e.message())); - } - - Ok("Details added".to_string()) -} - -#[post("/login", data = "")] -async fn login(conn: Connection<'_, Db>, login_form: Json, ip_addr: SocketAddr) -> Result> { - let db = conn.into_inner(); - println!("{} {}", login_form.application_id, login_form.password); - - let session_token = CandidateService::login( - db, - login_form.application_id, - login_form.password.to_string(), - ip_addr.ip().to_string() - ) - .await; - - session_token.map_err(|e| Custom(Status::from_code(e.code()).unwrap_or_default(), e.message())) -} - #[get("/hello")] async fn hello() -> &'static str { "Hello, world!" @@ -107,7 +35,24 @@ async fn start() -> Result<(), rocket::Error> { .attach(Db::init()) .attach(AdHoc::try_on_ignite("Migrations", run_migrations)) //.mount("/", FileServer::from(relative!("/static"))) - .mount("/", routes![create, login, hello, validate, fill_details, admin]) + .mount("/", routes![hello]) + .mount( + "/candidate/", + routes![ + routes::candidate::login, + routes::candidate::whoami, + routes::candidate::fill_details, + ], + ) + .mount( + "/admin/", + routes![ + routes::admin::login, + routes::admin::whoami, + routes::admin::hello, + routes::admin::create_candidate, + ], + ) .register("/", catchers![]) .launch() .await diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs new file mode 100644 index 0000000..eeb3bd7 --- /dev/null +++ b/api/src/routes/admin.rs @@ -0,0 +1,76 @@ +use std::net::SocketAddr; + +use portfolio_core::{ + crypto::random_8_char_string, + services::{admin_service::AdminService, candidate_service::CandidateService}, +}; +use requests::{AdminLoginRequest, RegisterRequest}; +use rocket::http::Status; +use rocket::response::status::Custom; +use rocket::serde::json::Json; + +use sea_orm_rocket::Connection; + +use crate::{guards::request::auth::AdminAuth, pool::Db, requests}; + +#[post("/login", data = "")] +pub async fn login( + conn: Connection<'_, Db>, + login_form: Json, + ip_addr: SocketAddr, +) -> Result> { + let db = conn.into_inner(); + println!("{} {}", login_form.admin_id, login_form.password); + + let session_token = AdminService::login( + db, + login_form.admin_id, + login_form.password.to_string(), + ip_addr.ip().to_string(), + ) + .await; + + session_token.map_err(|e| Custom(Status::from_code(e.code()).unwrap_or_default(), e.message())) +} + +#[get("/whoami")] +pub async fn whoami(session: AdminAuth) -> Result> { + let admin: entity::admin::Model = session.into(); + Ok(admin.id.to_string()) +} + +#[get("/hello")] +pub async fn hello(_session: AdminAuth) -> Result> { + Ok("Hello admin".to_string()) +} + +#[post("/create", data = "")] +pub async fn create_candidate( + conn: Connection<'_, Db>, + _session: AdminAuth, + post_form: Json, +) -> Result> { + let db = conn.into_inner(); + let form = post_form.into_inner(); + + let plain_text_password = random_8_char_string(); + + let candidate = CandidateService::create( + db, + form.application_id, + &plain_text_password, + form.personal_id_number, + ) + .await; + + if candidate.is_err() { + // TODO cleanup + let e = candidate.err().unwrap(); + return Err(Custom( + Status::from_code(e.code()).unwrap_or_default(), + e.message(), + )); + } + + Ok(plain_text_password) +} diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs new file mode 100644 index 0000000..2ff7623 --- /dev/null +++ b/api/src/routes/candidate.rs @@ -0,0 +1,63 @@ +use std::net::SocketAddr; + +use portfolio_core::services::candidate_service::{CandidateService, UserDetails}; +use requests::LoginRequest; +use rocket::http::Status; +use rocket::response::status::Custom; +use rocket::serde::json::Json; + +use sea_orm_rocket::Connection; + +use crate::{guards::request::auth::CandidateAuth, pool::Db, requests}; + +#[post("/login", data = "")] +pub async fn login( + conn: Connection<'_, Db>, + login_form: Json, + ip_addr: SocketAddr, +) -> Result> { + let db = conn.into_inner(); + println!("{} {}", login_form.application_id, login_form.password); + + let session_token = CandidateService::login( + db, + login_form.application_id, + login_form.password.to_string(), + ip_addr.ip().to_string(), + ) + .await; + + session_token.map_err(|e| Custom(Status::from_code(e.code()).unwrap_or_default(), e.message())) +} + + +#[get("/whoami")] +pub async fn whoami(session: CandidateAuth) -> Result> { + let candidate: entity::candidate::Model = session.into(); + Ok(candidate.application.to_string()) +} + +#[put("/details", data = "
")] +pub async fn fill_details( + conn: Connection<'_, Db>, + details: Json, + session: CandidateAuth, +) -> Result> { + let db = conn.into_inner(); + let form = details.into_inner(); + let candidate: entity::candidate::Model = session.into(); + + let candidate = CandidateService::add_user_details(db, candidate, form).await; + + if candidate.is_err() { + // TODO cleanup + let e = candidate.err().unwrap(); + return Err(Custom( + Status::from_code(e.code()).unwrap_or_default(), + e.message(), + )); + } + + Ok("Details added".to_string()) +} + diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index e69de29..90ca91f 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -0,0 +1,2 @@ +pub mod admin; +pub mod candidate; \ No newline at end of file From 6f608fc8dfd9c2a60fc2b6f3ed7d25f751798174 Mon Sep 17 00:00:00 2001 From: EETagent Date: Mon, 7 Nov 2022 13:45:47 +0100 Subject: [PATCH 3/4] feat: set cookies on ok login request --- api/Cargo.toml | 1 + api/src/routes/admin.rs | 16 ++++++++++++++-- api/src/routes/candidate.rs | 18 ++++++++++++++---- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/api/Cargo.toml b/api/Cargo.toml index d102cb2..ac54819 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dependencies] rocket = { version = "^0.5.0-rc.2", features = [ "json", + "secrets", ] } async-stream = { version = "^0.3" } diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index eeb3bd7..ee06626 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -5,7 +5,7 @@ use portfolio_core::{ services::{admin_service::AdminService, candidate_service::CandidateService}, }; use requests::{AdminLoginRequest, RegisterRequest}; -use rocket::http::Status; +use rocket::http::{Cookie, Status, CookieJar}; use rocket::response::status::Custom; use rocket::serde::json::Json; @@ -18,6 +18,7 @@ pub async fn login( conn: Connection<'_, Db>, login_form: Json, ip_addr: SocketAddr, + cookies: &CookieJar<'_>, ) -> Result> { let db = conn.into_inner(); println!("{} {}", login_form.admin_id, login_form.password); @@ -30,7 +31,18 @@ pub async fn login( ) .await; - session_token.map_err(|e| Custom(Status::from_code(e.code()).unwrap_or_default(), e.message())) + if let Err(e) = session_token { + return Err(Custom( + Status::from_code(e.code()).unwrap_or(Status::InternalServerError), + e.to_string(), + )); + } else { + let session_token = session_token.unwrap(); + // Todo: Add private? + cookies.add(Cookie::new("id", session_token.clone())); + + return Ok(session_token); + } } #[get("/whoami")] diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index 2ff7623..ccdc0df 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -2,7 +2,7 @@ use std::net::SocketAddr; use portfolio_core::services::candidate_service::{CandidateService, UserDetails}; use requests::LoginRequest; -use rocket::http::Status; +use rocket::http::{Cookie, CookieJar, Status}; use rocket::response::status::Custom; use rocket::serde::json::Json; @@ -15,6 +15,7 @@ pub async fn login( conn: Connection<'_, Db>, login_form: Json, ip_addr: SocketAddr, + cookies: &CookieJar<'_>, ) -> Result> { let db = conn.into_inner(); println!("{} {}", login_form.application_id, login_form.password); @@ -27,9 +28,19 @@ pub async fn login( ) .await; - session_token.map_err(|e| Custom(Status::from_code(e.code()).unwrap_or_default(), e.message())) -} + if let Err(e) = session_token { + return Err(Custom( + Status::from_code(e.code()).unwrap_or(Status::InternalServerError), + e.to_string(), + )); + } else { + let session_token = session_token.unwrap(); + // Todo: Add private? + cookies.add(Cookie::new("id", session_token.clone())); + return Ok(session_token); + } +} #[get("/whoami")] pub async fn whoami(session: CandidateAuth) -> Result> { @@ -60,4 +71,3 @@ pub async fn fill_details( Ok("Details added".to_string()) } - From 0cf6c4418d65648f5fb4ed1fe1eafbf06b0c30cf Mon Sep 17 00:00:00 2001 From: EETagent Date: Mon, 7 Nov 2022 13:56:25 +0100 Subject: [PATCH 4/4] feat: do not unwrap cookies in guards, use private cookies --- api/src/guards/request/auth/admin.rs | 9 ++++++++- api/src/guards/request/auth/candidate.rs | 18 +++++++++++++----- api/src/routes/admin.rs | 3 +-- api/src/routes/candidate.rs | 3 +-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/api/src/guards/request/auth/admin.rs b/api/src/guards/request/auth/admin.rs index 18ce925..0918ed7 100644 --- a/api/src/guards/request/auth/admin.rs +++ b/api/src/guards/request/auth/admin.rs @@ -19,7 +19,14 @@ impl Into for AdminAuth { impl<'r> FromRequest<'r> for AdminAuth { type Error = Option; async fn from_request(req: &'r Request<'_>) -> Outcome { - let session_id = req.cookies().get("id").unwrap().name_value().1; + let cookie = req.cookies().get_private("id"); + + let Some(cookie) = cookie else { + return Outcome::Failure((Status::Unauthorized, None)); + }; + + let session_id = cookie.name_value().1; + let conn = &req.rocket().state::().unwrap().conn; let uuid = match Uuid::parse_str(&session_id) { diff --git a/api/src/guards/request/auth/candidate.rs b/api/src/guards/request/auth/candidate.rs index 5b3200c..f02a1e8 100644 --- a/api/src/guards/request/auth/candidate.rs +++ b/api/src/guards/request/auth/candidate.rs @@ -14,12 +14,21 @@ impl Into for CandidateAuth { self.0 } } - + #[rocket::async_trait] impl<'r> FromRequest<'r> for CandidateAuth { type Error = Option; - async fn from_request(req: &'r Request<'_>) -> Outcome { - let session_id = req.cookies().get("id").unwrap().name_value().1; + async fn from_request( + req: &'r Request<'_>, + ) -> Outcome { + let cookie = req.cookies().get_private("id"); + + let Some(cookie) = cookie else { + return Outcome::Failure((Status::Unauthorized, None)); + }; + + let session_id = cookie.name_value().1; + let conn = &req.rocket().state::().unwrap().conn; let uuid = match Uuid::parse_str(&session_id) { @@ -33,6 +42,5 @@ impl<'r> FromRequest<'r> for CandidateAuth { Ok(model) => Outcome::Success(CandidateAuth(model)), Err(_) => Outcome::Failure((Status::Unauthorized, None)), } - } -} \ No newline at end of file +} diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index ee06626..9f4f1ba 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -38,8 +38,7 @@ pub async fn login( )); } else { let session_token = session_token.unwrap(); - // Todo: Add private? - cookies.add(Cookie::new("id", session_token.clone())); + cookies.add_private(Cookie::new("id", session_token.clone())); return Ok(session_token); } diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index ccdc0df..1f2ccfb 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -35,8 +35,7 @@ pub async fn login( )); } else { let session_token = session_token.unwrap(); - // Todo: Add private? - cookies.add(Cookie::new("id", session_token.clone())); + cookies.add_private(Cookie::new("id", session_token.clone())); return Ok(session_token); }