diff --git a/api/src/guard/candidate_refresh_token.rs b/api/src/guard/candidate_refresh_token.rs deleted file mode 100644 index dee824d..0000000 --- a/api/src/guard/candidate_refresh_token.rs +++ /dev/null @@ -1,27 +0,0 @@ -use portfolio_core::sea_orm::prelude::Uuid; -use rocket::http::Status; -use rocket::outcome::Outcome; -use rocket::request::{FromRequest, Request}; - - -pub struct UUIDCookie(Uuid); - -impl UUIDCookie { - pub fn value(self) -> Uuid { - self.0 - } -} - -#[rocket::async_trait] -impl<'r> FromRequest<'r> for UUIDCookie { - type Error = Status; - async fn from_request(req: &'r Request<'_>) -> Outcome { - let session_id = req.cookies().get("id").unwrap().name_value().1; - println!("session_id: {}", session_id); - - match Uuid::parse_str(&session_id) { - Ok(uuid) => Outcome::Success(UUIDCookie(uuid)), - Err(_) => return Outcome::Failure((Status::BadRequest, Status::BadRequest)), - } - } -} diff --git a/api/src/guard/mod.rs b/api/src/guard/mod.rs deleted file mode 100644 index bdf6937..0000000 --- a/api/src/guard/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod candidate_refresh_token; \ No newline at end of file diff --git a/api/src/guards/data/letter.rs b/api/src/guards/data/letter.rs new file mode 100644 index 0000000..d70b00c --- /dev/null +++ b/api/src/guards/data/letter.rs @@ -0,0 +1,43 @@ +use rocket::data::{self, Data, FromData, ToByteUnit}; +use rocket::http::{ContentType, Status}; +use rocket::outcome::Outcome; +use rocket::request::Request; + +struct Letter(Vec); + +impl Into> for Letter { + fn into(self) -> Vec { + self.0 + } +} + +#[rocket::async_trait] +impl<'r> FromData<'r> for Letter { + type Error = Option; + + async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> data::Outcome<'r, Self> { + let content_type_pdf = ContentType::new("application", "application/pdf"); + + if req.content_type() != Some(&content_type_pdf) { + return Outcome::Failure((Status::BadRequest, None)) + } + + let data = data.open(11.megabytes()); + + let data_bytes = data.into_bytes().await.unwrap(); + + if !data_bytes.is_complete() { + // TODO: Over limit + } + + let data_bytes = data_bytes.into_inner(); + + let is_pdf = portfolio_core::filetype::filetype_is_pdf(&data_bytes); + + if !is_pdf { + // TODO: Not PDF + } + + Outcome::Success(Letter(data_bytes)) + } +} diff --git a/api/src/guards/data/mod.rs b/api/src/guards/data/mod.rs new file mode 100644 index 0000000..d305854 --- /dev/null +++ b/api/src/guards/data/mod.rs @@ -0,0 +1,2 @@ +pub mod portfolio; +pub mod letter; \ No newline at end of file diff --git a/api/src/guards/data/portfolio.rs b/api/src/guards/data/portfolio.rs new file mode 100644 index 0000000..3738ee1 --- /dev/null +++ b/api/src/guards/data/portfolio.rs @@ -0,0 +1,43 @@ +use rocket::data::{self, Data, FromData, ToByteUnit}; +use rocket::http::{ContentType, Status}; +use rocket::outcome::Outcome; +use rocket::request::Request; + +struct Portfolio(Vec); + +impl Into> for Portfolio { + fn into(self) -> Vec { + self.0 + } +} + +#[rocket::async_trait] +impl<'r> FromData<'r> for Portfolio { + type Error = Option; + + async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> data::Outcome<'r, Self> { + let content_type_zip = ContentType::new("application", "application/zip"); + + if req.content_type() != Some(&content_type_zip) { + return Outcome::Failure((Status::BadRequest, None)) + } + + let data = data.open(101.megabytes()); + + let data_bytes = data.into_bytes().await.unwrap(); + + if !data_bytes.is_complete() { + // TODO: Over limit + } + + let data_bytes = data_bytes.into_inner(); + + let is_zip = portfolio_core::filetype::filetype_is_zip(&data_bytes); + + if !is_zip { + // TODO: Not ZIP + } + + Outcome::Success(Portfolio(data_bytes)) + } +} diff --git a/api/src/guards/mod.rs b/api/src/guards/mod.rs new file mode 100644 index 0000000..0d72c60 --- /dev/null +++ b/api/src/guards/mod.rs @@ -0,0 +1,2 @@ +pub mod data; +pub mod request; \ No newline at end of file diff --git a/api/src/guards/request/mod.rs b/api/src/guards/request/mod.rs new file mode 100644 index 0000000..066d9a7 --- /dev/null +++ b/api/src/guards/request/mod.rs @@ -0,0 +1 @@ +pub mod session_auth; \ No newline at end of file diff --git a/api/src/guards/request/session_auth.rs b/api/src/guards/request/session_auth.rs new file mode 100644 index 0000000..273c360 --- /dev/null +++ b/api/src/guards/request/session_auth.rs @@ -0,0 +1,38 @@ +use entity::candidate::Model as Candidate; +use portfolio_core::sea_orm::prelude::Uuid; +use portfolio_core::services::candidate_service::CandidateService; +use rocket::http::Status; +use rocket::outcome::Outcome; +use rocket::request::{FromRequest, Request}; + +use crate::pool::Db; + +pub struct SessionAuth(Candidate); + +impl Into for SessionAuth { + fn into(self) -> Candidate { + self.0 + } +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for SessionAuth { + type Error = Option; + async fn from_request(req: &'r Request<'_>) -> Outcome { + let session_id = req.cookies().get("id").unwrap().name_value().1; + let conn = &req.rocket().state::().unwrap().conn; + + let uuid = match Uuid::parse_str(&session_id) { + Ok(uuid) => uuid, + Err(_) => return Outcome::Failure((Status::BadRequest, None)), + }; + + let session = CandidateService::auth_user_session(conn, uuid).await; + + match session { + Ok(model) => Outcome::Success(SessionAuth(model)), + Err(_) => Outcome::Failure((Status::Unauthorized, None)), + } + + } +} diff --git a/api/src/lib.rs b/api/src/lib.rs index 33136ec..47e615a 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -3,6 +3,7 @@ extern crate rocket; use std::net::SocketAddr; +use guards::request::session_auth::SessionAuth; use portfolio_core::error::ServiceError; use portfolio_core::services::candidate_service::CandidateService; use requests::{LoginRequest, RegisterRequest}; @@ -18,8 +19,9 @@ use sea_orm_rocket::{Connection, Database}; mod pool; -mod guard; +mod guards; mod requests; +mod routes; use pool::Db; @@ -28,8 +30,6 @@ pub use entity::candidate::Entity as Candidate; use portfolio_core::crypto::random_8_char_string; -use crate::guard::candidate_refresh_token::UUIDCookie; - fn custom_err_from_service_err(service_err: ServiceError) -> Custom { Custom(Status::from_code(service_err.0.code).unwrap_or_default(), service_err.1.to_string()) } @@ -49,15 +49,9 @@ async fn create(conn: Connection<'_, Db>, post_form: Json) -> R } #[get("/whoami")] -async fn validate(conn: Connection<'_, Db>, uuid_cookie: Result) -> Result> { - let db = conn.into_inner(); - let user = CandidateService::auth_user_session(db, uuid_cookie.ok().unwrap().value()).await; - - - match user { - Ok(user) => Ok(user.application.to_string()), - Err(err) => Err(custom_err_from_service_err(err)) - } +async fn validate(session: SessionAuth) -> Result> { + let candidate: entity::candidate::Model = session.into(); + Ok(candidate.application.to_string()) } #[post("/login", data = "")] diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/core/src/database/mod.rs b/core/src/database/mod.rs new file mode 100644 index 0000000..f1dee11 --- /dev/null +++ b/core/src/database/mod.rs @@ -0,0 +1,2 @@ +pub mod mutation; +pub mod query; \ No newline at end of file diff --git a/core/src/mutation.rs b/core/src/database/mutation/candidate.rs similarity index 74% rename from core/src/mutation.rs rename to core/src/database/mutation/candidate.rs index ba914fa..55d870c 100644 --- a/core/src/mutation.rs +++ b/core/src/database/mutation/candidate.rs @@ -1,11 +1,9 @@ +use crate::Mutation; use std::vec; -use chrono::{Utc, Duration}; -use ::entity::{candidate, session}; -use sea_orm::{*, prelude::Uuid}; -use crate::crypto::{hash_password, self}; - -pub struct Mutation; +use ::entity::candidate; +use sea_orm::{*}; +use crate::{crypto::{hash_password, self}}; impl Mutation { pub async fn create_candidate( @@ -37,36 +35,6 @@ impl Mutation { .insert(db) .await } - - - pub async fn insert_session( - db: &DbConn, - user_id: i32, - random_uuid: Uuid, - ip_addr: String, - ) -> Result { - session::ActiveModel { - id: Set(random_uuid), - user_id: Set(user_id), - ip_address: Set(ip_addr), - created_at: Set(Utc::now().naive_local()), - expires_at: Set(Utc::now().naive_local().checked_add_signed(Duration::days(1)).unwrap()), - } - .insert(db) - .await - } - - pub async fn delete_session( - db: &DbConn, - session_id: Uuid - ) -> Result { - session::ActiveModel { - id: Set(session_id), - ..Default::default() - } - .delete(db) - .await - } } diff --git a/core/src/database/mutation/mod.rs b/core/src/database/mutation/mod.rs new file mode 100644 index 0000000..633a325 --- /dev/null +++ b/core/src/database/mutation/mod.rs @@ -0,0 +1,4 @@ +pub struct Mutation; + +pub mod session; +pub mod candidate; \ No newline at end of file diff --git a/core/src/database/mutation/session.rs b/core/src/database/mutation/session.rs new file mode 100644 index 0000000..db82b4b --- /dev/null +++ b/core/src/database/mutation/session.rs @@ -0,0 +1,37 @@ +use chrono::{Utc, Duration}; +use ::entity::session; +use sea_orm::{*, prelude::Uuid}; + +use crate::Mutation; + + +impl Mutation { + pub async fn insert_session( + db: &DbConn, + user_id: i32, + random_uuid: Uuid, + ip_addr: String, + ) -> Result { + session::ActiveModel { + id: Set(random_uuid), + user_id: Set(user_id), + ip_address: Set(ip_addr), + created_at: Set(Utc::now().naive_local()), + expires_at: Set(Utc::now() + .naive_local() + .checked_add_signed(Duration::days(1)) + .unwrap()), + } + .insert(db) + .await + } + + pub async fn delete_session(db: &DbConn, session_id: Uuid) -> Result { + session::ActiveModel { + id: Set(session_id), + ..Default::default() + } + .delete(db) + .await + } +} diff --git a/core/src/query.rs b/core/src/database/query/candidate.rs similarity index 56% rename from core/src/query.rs rename to core/src/database/query/candidate.rs index 0c0c440..a8b5552 100644 --- a/core/src/query.rs +++ b/core/src/database/query/candidate.rs @@ -1,44 +1,35 @@ -use ::entity::{candidate, candidate::Entity as Candidate}; -use ::entity::{session, session::Entity as Session}; -use sea_orm::*; -use sea_orm::prelude::Uuid; +use crate::Query; -pub struct Query; +use ::entity::{candidate, candidate::Entity as Candidate}; +use sea_orm::*; impl Query { - pub async fn find_candidate_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + pub async fn find_candidate_by_id( + db: &DbConn, + id: i32, + ) -> Result, DbErr> { Candidate::find_by_id(id).one(db).await } - - pub async fn find_session_by_uuid(db: &DbConn, uuid: Uuid) -> Result, DbErr> { - Session::find_by_id(uuid).one(db).await - } - - // find session by user id - pub async fn find_sessions_by_user_id(db: &DbConn, user_id: i32) -> Result, DbErr> { - Session::find() - .filter(session::Column::UserId.eq(user_id)) - .all(db) - .await - } } #[cfg(test)] mod tests { - use sea_orm::{DbConn, Set, ActiveModelTrait}; use entity::candidate; - use sea_orm::{Schema, Database, DbBackend, sea_query::TableCreateStatement, ConnectionTrait}; + use sea_orm::{sea_query::TableCreateStatement, ConnectionTrait, Database, DbBackend, Schema}; + use sea_orm::{ActiveModelTrait, DbConn, Set}; use crate::Query; - + #[cfg(test)] async fn get_memory_sqlite_connection() -> DbConn { let base_url = "sqlite::memory:"; let db: DbConn = Database::connect(base_url).await.unwrap(); - + let schema = Schema::new(DbBackend::Sqlite); let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); - db.execute(db.get_database_backend().build(&stmt)).await.unwrap(); + db.execute(db.get_database_backend().build(&stmt)) + .await + .unwrap(); db } @@ -55,11 +46,13 @@ mod tests { updated_at: Set(chrono::offset::Local::now().naive_local()), ..Default::default() } - .insert(&db) + .insert(&db) + .await + .unwrap(); + + let candidate = Query::find_candidate_by_id(&db, candidate.application) .await .unwrap(); - - let candidate = Query::find_candidate_by_id(&db, candidate.application).await.unwrap(); assert!(candidate.is_some()); } -} \ No newline at end of file +} diff --git a/core/src/database/query/mod.rs b/core/src/database/query/mod.rs new file mode 100644 index 0000000..df4cb4a --- /dev/null +++ b/core/src/database/query/mod.rs @@ -0,0 +1,4 @@ +pub struct Query; + +pub mod candidate; +pub mod session; \ No newline at end of file diff --git a/core/src/database/query/session.rs b/core/src/database/query/session.rs new file mode 100644 index 0000000..23ec16c --- /dev/null +++ b/core/src/database/query/session.rs @@ -0,0 +1,37 @@ +use crate::Query; + +use ::entity::{session, session::Entity as Session}; +use sea_orm::*; +use sea_orm::prelude::Uuid; + +impl Query { + pub async fn find_session_by_uuid(db: &DbConn, uuid: Uuid) -> Result, DbErr> { + Session::find_by_id(uuid).one(db).await + } + + // find session by user id + pub async fn find_sessions_by_user_id(db: &DbConn, user_id: i32) -> Result, DbErr> { + Session::find() + .filter(session::Column::UserId.eq(user_id)) + .all(db) + .await + } +} + +#[cfg(test)] +mod tests { + use sea_orm::DbConn; + use entity::candidate; + use sea_orm::{Schema, Database, DbBackend, sea_query::TableCreateStatement, ConnectionTrait}; + + #[cfg(test)] + async fn get_memory_sqlite_connection() -> DbConn { + let base_url = "sqlite::memory:"; + let db: DbConn = Database::connect(base_url).await.unwrap(); + + let schema = Schema::new(DbBackend::Sqlite); + let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); + db.execute(db.get_database_backend().build(&stmt)).await.unwrap(); + db + } +} \ No newline at end of file diff --git a/core/src/lib.rs b/core/src/lib.rs index 1773217..1121e29 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,11 +1,10 @@ -mod mutation; -mod query; +pub mod database; pub mod crypto; pub mod filetype; pub mod services; pub mod error; -pub use mutation::*; -pub use query::*; +pub use database::mutation::*; +pub use database::query::*; pub use sea_orm; diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 91bdd83..3058a4a 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -8,6 +8,7 @@ use crate::{crypto::{self}, Query, error::{ServiceError, USER_NOT_FOUND_ERROR, I pub struct CandidateService; impl CandidateService { + /// Delete n old sessions for user async fn delete_old_sessions(db: &DatabaseConnection, user_id: i32, keep_n_recent: usize) -> Result<(), ServiceError> { let mut sessions = Query::find_sessions_by_user_id(db, user_id).await.unwrap(); @@ -21,6 +22,7 @@ impl CandidateService { Ok(()) } + /// Authenticate user by application id and password and generate a new session pub async fn new_session(db: &DatabaseConnection, user_id: i32, password: String, ip_addr: String) -> Result { let candidate = match Query::find_candidate_by_id(db, user_id).await { Ok(candidate) => match candidate { @@ -40,10 +42,8 @@ impl CandidateService { Err(_) => {return Err(INVALID_CREDENTIALS_ERROR)} } - // TODO delete old sessions? - - // user is authenticated, generate a session + // user is authenticated, generate a new session let random_uuid: Uuid = Uuid::new_v4(); let session = match Mutation::insert_session(db, user_id, random_uuid, ip_addr).await { @@ -57,6 +57,8 @@ impl CandidateService { Ok(session.id.to_string()) } + /// Authenticate user by session id + /// Return user model if session is valid pub async fn auth_user_session(db: &DatabaseConnection, uuid: Uuid) -> Result { let session = match Query::find_session_by_uuid(db, uuid).await { Ok(session) => match session {