diff --git a/Cargo.lock b/Cargo.lock index 2339016..deb4d1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.72", ] [[package]] @@ -220,7 +220,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn", + "syn 2.0.72", ] [[package]] @@ -439,6 +439,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.72", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.72", +] + [[package]] name = "der" version = "0.7.9" @@ -642,7 +677,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.72", ] [[package]] @@ -921,6 +956,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -950,6 +991,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1326,7 +1376,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.72", ] [[package]] @@ -1383,6 +1433,30 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -1402,6 +1476,7 @@ dependencies = [ "chrono", "dotenvy", "futures", + "itertools", "jsonwebtoken", "rand", "scrypt", @@ -1417,6 +1492,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "validator", ] [[package]] @@ -1670,7 +1746,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.72", ] [[package]] @@ -1884,7 +1960,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.72", ] [[package]] @@ -1907,7 +1983,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.72", "tempfile", "tokio", "url", @@ -2033,12 +2109,28 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.72" @@ -2092,7 +2184,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.72", ] [[package]] @@ -2176,7 +2268,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.72", ] [[package]] @@ -2273,7 +2365,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.72", ] [[package]] @@ -2382,6 +2474,36 @@ dependencies = [ "serde", ] +[[package]] +name = "validator" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55591299b7007f551ed1eb79a684af7672c19c3193fb9e0a31936987bb2438ec" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "valuable" version = "0.1.0" @@ -2433,7 +2555,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.72", "wasm-bindgen-shared", ] @@ -2455,7 +2577,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.72", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2670,7 +2792,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.72", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fe7d612..0fcb12c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ axum-extra = { version = "0.9", features = ["typed-header"] } chrono = { version = "0.4", features = ["serde"] } dotenvy = "0.15" futures = "0.3" +itertools = "0.13.0" jsonwebtoken = "9" rand = "0.8" scrypt = { version = "0.11", default-features = false, features = ["std"] } @@ -52,3 +53,4 @@ tracing-subscriber = { version = "0.3", features = [ "env-filter", ] } uuid = { version = "1", features = ["serde", "v7"] } +validator = { version = "0.18", features = ["derive"] } diff --git a/sql/get_folders.sql b/sql/get_folders.sql index 084f0c5..f17eca1 100644 --- a/sql/get_folders.sql +++ b/sql/get_folders.sql @@ -5,7 +5,7 @@ SELECT created_at FROM folders f - JOIN permissions p ON f.folder_id = p.folder_id + LEFT JOIN permissions p ON f.folder_id = p.folder_id WHERE parent_folder_id = $1 - AND p.user_id = $2 \ No newline at end of file + AND (p.user_id = $2 OR f.owner_id = $2) \ No newline at end of file diff --git a/src/auth.rs b/src/auth.rs index 189540a..19460a7 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -10,6 +10,7 @@ use axum_extra::{ headers::{authorization::Bearer, Authorization}, TypedHeader, }; +use chrono::{TimeDelta, Utc}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use rand::{rngs::OsRng, RngCore}; use serde::{Deserialize, Serialize}; @@ -122,6 +123,13 @@ pub struct Claims { } impl Claims { + pub fn new(user_id: i32) -> Self { + Self { + user_id, + exp: (Utc::now() + TimeDelta::days(30)).timestamp(), + } + } + pub fn encode(self) -> Result { let access_token = encode( &Header::new(jsonwebtoken::Algorithm::HS256), diff --git a/src/db/users.rs b/src/db/users.rs index add0e8e..4e55314 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -10,16 +10,19 @@ pub async fn create_user( user_email: &str, hashed_password: &[u8], pool: &Pool, -) -> sqlx::Result { - let id = sqlx::query!( +) -> sqlx::Result> { + let Some(record) = sqlx::query!( "INSERT INTO users(username, email, hashed_password) VALUES ($1, $2, $3) RETURNING user_id", user_name, user_email, hashed_password ) - .fetch_one(pool) + .fetch_optional(pool) .await? - .user_id; + else { + return Ok(None); + }; + let id = record.user_id; sqlx::query!( "INSERT INTO folders(owner_id, folder_name) VALUES ($1, $2)", id, @@ -27,7 +30,7 @@ pub async fn create_user( ) .execute(pool) .await?; - Ok(id) + Ok(Some(id)) } /// Deletes the user and returns the files that must be deleted diff --git a/src/endpoints/authorization/auth_post.rs b/src/endpoints/authorization/auth_post.rs index 72381bc..00e942d 100644 --- a/src/endpoints/authorization/auth_post.rs +++ b/src/endpoints/authorization/auth_post.rs @@ -1,5 +1,4 @@ use axum::Form; -use chrono::TimeDelta; use crate::{ auth::{authenticate_user, Error, Token}, @@ -12,24 +11,13 @@ pub struct Params { password: String, } -fn get_exp() -> i64 { - let mut time = chrono::Utc::now(); - time += TimeDelta::days(30); - time.timestamp() -} - pub async fn post( - State(state): State, + State(pool): State, Form(payload): Form, ) -> Result, Error> { - let user_id = authenticate_user(&payload.username, &payload.password, &state.pool) + let user_id = authenticate_user(&payload.username, &payload.password, &pool) .await .map_err(|_| Error::WrongCredentials)? .ok_or(Error::WrongCredentials)?; - Claims { - user_id, - exp: get_exp(), - } - .encode() - .map(Json) + Claims::new(user_id).encode().map(Json) } diff --git a/src/endpoints/authorization/mod.rs b/src/endpoints/authorization/mod.rs index 8052f27..d7dc2a2 100644 --- a/src/endpoints/authorization/mod.rs +++ b/src/endpoints/authorization/mod.rs @@ -1 +1,2 @@ pub mod auth_post; +pub mod register; diff --git a/src/endpoints/authorization/register.rs b/src/endpoints/authorization/register.rs new file mode 100644 index 0000000..0130b89 --- /dev/null +++ b/src/endpoints/authorization/register.rs @@ -0,0 +1,68 @@ +use axum::Form; +use axum_extra::either::Either; +use itertools::Itertools; +use validator::{Validate, ValidationError}; + +use crate::{ + auth::{Error, HashedBytes, Token}, + prelude::*, +}; + +#[derive(Deserialize, Debug, Validate)] +pub struct Params { + #[validate(length(min = 3, max = 10))] + username: String, + #[validate(email)] + email: String, + #[validate(length(min = 6), custom(function = "validate_password"))] + password: String, +} + +fn validate_password(password: &str) -> Result<(), ValidationError> { + let mut has_lower = false; + let mut has_upper = false; + let mut has_number = false; + let mut has_special = false; + for char in password.chars() { + if char.is_lowercase() { + has_lower = true; + } else if char.is_uppercase() { + has_upper = true; + } else if char.is_ascii_digit() { + has_number = true; + } else { + has_special = true; + } + } + let error_msgs = [has_lower, has_upper, has_number, has_special] + .into_iter() + .zip(["No lower", "No upper", "No numbers", "No special"]) + .filter_map(|(param, msg)| (!param).then_some(msg)); + let msg = error_msgs.format(" ").to_string(); + if !msg.is_empty() { + return Err(ValidationError::new("invalid_password").with_message(msg.into())); + } + Ok(()) +} + +pub async fn register( + State(pool): State, + Form(params): Form, +) -> Result, Either<(StatusCode, String), Error>> { + params + .validate() + .map_err(|err| Either::E1((StatusCode::BAD_REQUEST, err.to_string())))?; + let password = HashedBytes::hash_bytes(params.password.as_bytes()).as_bytes(); + let Some(id) = db::users::create_user(¶ms.username, ¶ms.email, &password, &pool) + .await + .handle_internal() + .map_err(|status| Either::E1((status, String::new())))? + else { + return Err(Either::E1(( + StatusCode::BAD_REQUEST, + "Either the user name or the email are taken".to_owned(), + ))); + }; + let token = Claims::new(id).encode().map_err(Either::E2)?; + Ok(Json(token)) +} diff --git a/src/endpoints/folder/get_structure.rs b/src/endpoints/folder/get_structure.rs index 3dbb8c7..812df99 100644 --- a/src/endpoints/folder/get_structure.rs +++ b/src/endpoints/folder/get_structure.rs @@ -22,17 +22,11 @@ impl From for FolderStructure { } } -#[derive(Debug, Serialize)] -pub struct Response { - folder_id: Uuid, - structure: FolderStructure, -} - pub async fn structure( Query(params): Query, State(pool): State, claims: Claims, -) -> Result, StatusCode> { +) -> Result, StatusCode> { let folder_id = db::folder::process_id(params.folder_id, claims.user_id, &pool) .await .handle_internal()? @@ -41,15 +35,12 @@ pub async fn structure( .await .handle_internal()? .ok_or(StatusCode::NOT_FOUND)?; - let mut response = Response { - folder_id, - structure: folder.into(), - }; - let mut stack = vec![&mut response.structure]; + let mut response: FolderStructure = folder.into(); + let mut stack = vec![&mut response]; while let Some(folder) = stack.pop() { let (files, folders) = try_join!( - db::file::get_files(folder_id, &pool).try_collect(), - db::folder::get_folders(folder_id, claims.user_id, &pool) + db::file::get_files(folder.folder_base.folder_id, &pool).try_collect(), + db::folder::get_folders(folder.folder_base.folder_id, claims.user_id, &pool) .map_ok(Into::into) .try_collect() ) diff --git a/src/endpoints/users/put.rs b/src/endpoints/users/put.rs index e62c5ec..8a8cdbf 100644 --- a/src/endpoints/users/put.rs +++ b/src/endpoints/users/put.rs @@ -1,8 +1,12 @@ +use validator::Validate; + use crate::prelude::*; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Validate)] pub struct Params { + #[validate(email)] username: String, + #[validate(email)] email: String, } @@ -10,9 +14,13 @@ pub async fn put( State(pool): State, claims: Claims, Json(params): Json, -) -> Result, StatusCode> { +) -> Result, (StatusCode, String)> { + params + .validate() + .map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()))?; let info = db::users::update(claims.user_id, ¶ms.username, ¶ms.email, &pool) .await - .handle_internal()?; + .handle_internal() + .map_err(|status| (status, String::new()))?; Ok(Json(info)) } diff --git a/src/main.rs b/src/main.rs index edd1a03..ef788b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -121,6 +121,7 @@ fn app(state: AppState) -> Router { ) .route("/users/current", get(users::get::current)) .route("/users/search", get(users::search::search)) + .route("/register", post(authorization::register::register)) .route("/authorize", post(authorization::auth_post::post)) .layer(middleware) .with_state(state)