From b3ee37fbe3c1f89391c80ce9577fdfeb1408f21f Mon Sep 17 00:00:00 2001 From: acx Date: Sun, 28 Jul 2024 14:24:53 +0000 Subject: [PATCH] feat: tag --- Cargo.lock | 67 ++++++++ Cargo.toml | 2 + .../2024-07-07-151037_base_schema/up.sql | 2 +- .../handler.rs => ledger/category.rs} | 0 src/ledger/mod.rs | 2 + src/ledger/tag.rs | 146 ++++++++++++++++++ src/main.rs | 50 +++++- src/middleware/auth.rs | 4 +- src/model/db_model.rs | 37 +++++ src/model/mod.rs | 1 + src/model/req.rs | 0 src/user/dal.rs | 118 ++++++++++++++ src/user/handler.rs | 29 ++++ src/{category => user}/mod.rs | 1 + src/util/mod.rs | 1 + src/util/pass.rs | 16 ++ 16 files changed, 469 insertions(+), 7 deletions(-) rename src/{category/handler.rs => ledger/category.rs} (100%) create mode 100644 src/ledger/mod.rs create mode 100644 src/ledger/tag.rs create mode 100644 src/model/req.rs create mode 100644 src/user/dal.rs create mode 100644 src/user/handler.rs rename src/{category => user}/mod.rs (56%) create mode 100644 src/util/pass.rs diff --git a/Cargo.lock b/Cargo.lock index 7837383..0a6dbb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,6 +176,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "2.6.0" @@ -389,6 +395,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -544,6 +551,8 @@ dependencies = [ "dotenvy", "jsonwebtoken", "once_cell", + "pbkdf2", + "rand_core", "serde", "serde_json", "tokio", @@ -559,6 +568,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.1.0" @@ -871,6 +889,29 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "pem" version = "3.0.4" @@ -952,6 +993,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.5.2" @@ -1108,6 +1158,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1166,6 +1227,12 @@ 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 = "2.0.69" diff --git a/Cargo.toml b/Cargo.toml index 2682e59..468ea90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,5 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } once_cell = "1.19.0" axum-macros = "0.4.1" +pbkdf2 = { version = "0.12", features = ["simple"] } +rand_core ={version = "0.6", features = ["std"]} diff --git a/migrations/2024-07-07-151037_base_schema/up.sql b/migrations/2024-07-07-151037_base_schema/up.sql index 2158d63..d5ba287 100644 --- a/migrations/2024-07-07-151037_base_schema/up.sql +++ b/migrations/2024-07-07-151037_base_schema/up.sql @@ -72,7 +72,7 @@ CREATE TABLE "amounts" ( CREATE TABLE "users" ( "id" BIGSERIAL PRIMARY KEY, - "username" TEXT NOT NULL, + "username" TEXT NOT NULL UNIQUE, "password" TEXT NOT NULL, "mail" TEXT NOT NULL, "is_delete" BOOLEAN NOT NULL DEFAULT FALSE, diff --git a/src/category/handler.rs b/src/ledger/category.rs similarity index 100% rename from src/category/handler.rs rename to src/ledger/category.rs diff --git a/src/ledger/mod.rs b/src/ledger/mod.rs new file mode 100644 index 0000000..4e864ca --- /dev/null +++ b/src/ledger/mod.rs @@ -0,0 +1,2 @@ +pub mod category; +pub mod tag; \ No newline at end of file diff --git a/src/ledger/tag.rs b/src/ledger/tag.rs new file mode 100644 index 0000000..e44f736 --- /dev/null +++ b/src/ledger/tag.rs @@ -0,0 +1,146 @@ +// use std::sync::Arc; +use axum::routing::{get, post}; +use axum::{ + extract::{Path, State}, + http::StatusCode, + Json, Router, +}; +use axum_macros::debug_handler; +use diesel::prelude::*; +// use diesel::update; +use serde::{Deserialize, Serialize}; +// use serde_json::to_string; +use crate::model::db_model; +use crate::model::schema; +use crate::util; +use crate::util::req::CommonResp; +use chrono::prelude::*; +use tracing::info; +use crate::middleware::auth; +use crate::middleware::auth::Claims; + +#[derive(Deserialize)] +pub struct CreateTagRequest { + name: String, +} + +#[derive(Serialize)] +pub struct CreateTagResponse { + id: i64, + name: String, +} + +pub fn get_nest_handlers() -> Router { + Router::new() + .route("/", post(create_tag).get(get_all_tags)) + .route("/:id", post(update_tag).get(get_tag)) +} + +#[debug_handler] +pub async fn create_tag( + State(app_state): State, + claims: Claims, + Json(payload): Json, +) -> Result, (StatusCode, String)> { + let uid: i64 = claims.uid.clone(); + let conn = app_state + .db + .get() + .await + .map_err(util::req::internal_error)?; + let new_tag = db_model::TagForm { + name: payload.name, + uid, + }; + let res = conn + .interact(move |conn| { + diesel::insert_into(schema::tags::table) + .values(&new_tag) + .returning(db_model::Tag::as_returning()) + .get_result(conn) + }) + .await + .map_err(util::req::internal_error)? + .map_err(util::req::internal_error)?; + Ok(Json(res)) +} + +pub async fn update_tag( + Path(id): Path, + State(app_state): State, + claims: Claims, + Json(payload): Json, +) -> Result, (StatusCode, String)> { + let uid: i64 = claims.uid.clone(); + let conn = app_state + .db + .get() + .await + .map_err(util::req::internal_error)?; + let now = Utc::now().naive_utc(); + let res = conn + .interact(move |conn| { + diesel::update(schema::tags::table) + .filter(schema::tags::id.eq(id)) + .filter(schema::tags::uid.eq(uid)) + .set(( + schema::tags::name.eq(payload.name), + schema::tags::update_at.eq(now), + )) + .execute(conn) + }) + .await + .map_err(util::req::internal_error)? + .map_err(util::req::internal_error)?; + let resp = util::req::CommonResp { code: 0 }; + Ok(Json(resp)) +} + +pub async fn get_tag( + Path(id): Path, + State(app_state): State, + claims: Claims, +) -> Result, (StatusCode, String)> { + let uid: i64 = claims.uid.clone(); + let conn = app_state + .db + .get() + .await + .map_err(util::req::internal_error)?; + let res = conn + .interact(move |conn| { + schema::tags::table + .filter(schema::tags::id.eq(id)) + .filter(schema::tags::uid.eq(uid)) + .select(db_model::Tag::as_select()) + .limit(1) + .get_result(conn) + }) + .await + .map_err(util::req::internal_error)? + .map_err(util::req::internal_error)?; + Ok(Json(res)) +} + +pub async fn get_all_tags( + State(app_state): State, + claims: Claims, +) -> Result>, (StatusCode, String)> { + let uid: i64 = claims.uid.clone(); + let conn = app_state + .db + .get() + .await + .map_err(util::req::internal_error)?; + let res = conn + .interact(move |conn| { + schema::tags::table + .filter(schema::tags::uid.eq(uid)) + .select(db_model::Tag::as_select()) + .load(conn) + }) + .await + .map_err(util::req::internal_error)? + .map_err(util::req::internal_error)?; + Ok(Json(res)) +} diff --git a/src/main.rs b/src/main.rs index 7988cb8..1f3f696 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use std::env; use axum::{ // http::StatusCode, // routing::{get, post}, @@ -5,18 +6,21 @@ use axum::{ Router, }; use axum::http::Method; -use serde::{Deserialize, Serialize}; +// use pbkdf2::password_hash::Error; +// use serde::{Deserialize, Serialize}; use tower::ServiceBuilder; use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::TraceLayer; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use crate::util::pass::get_pbkdf2_from_psw; // Project modules -mod category; +mod ledger; mod middleware; mod model; mod util; +mod user; // Passed App State #[derive(Clone)] @@ -30,6 +34,12 @@ async fn main() { tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer()) .init(); + + let args: Vec = env::args().collect(); + + if args.len() <= 1 { + return; + } // initialize db connection let db_url = std::env::var("DATABASE_URL").unwrap(); @@ -39,6 +49,37 @@ async fn main() { .unwrap(); let shared_state = AppState { db: pool }; + let cmd = args[1].clone(); + + match cmd.as_str() { + "add_user" => { + println!("adding user"); + if args.len() <= 4 { + println!("insufficient arg number"); + return; + } + let user = args[2].clone(); + let psw = args[3].clone(); + let mail = args[4].clone(); + println!("adding user {}", user); + let hashed = get_pbkdf2_from_psw(psw); + let mut hash_psw = "".to_string(); + match hashed { + Ok(val) => { + println!("get hash {}", val); + hash_psw=val; + } + Err(_) => {} + } + let res = user::dal::add_user(shared_state, user, hash_psw, mail) + .await; + return; + } + _ => { + println!("unknown command {}", cmd); + } + } + // Register routers let cors_layer = CorsLayer::new() @@ -50,8 +91,9 @@ async fn main() { let app = Router::new() // V1 apis - .nest("/api/v1/category", category::handler::get_nest_handlers()) - .nest("/api/v1/v2", category::handler::get_nest_handlers()) + .nest("/api/v1/category", ledger::category::get_nest_handlers()) + .nest("/api/v1/tag", ledger::tag::get_nest_handlers()) + .nest("/api/v1/user", user::handler::get_nest_handlers()) .with_state(shared_state) .layer(global_layer); diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs index b9e12c3..0340594 100644 --- a/src/middleware/auth.rs +++ b/src/middleware/auth.rs @@ -22,7 +22,7 @@ use crate::util; #[derive(Debug, Serialize, Deserialize)] pub struct Claims { sub: String, - company: String, + // company: String, exp: usize, pub uid: i64, } @@ -68,7 +68,7 @@ impl Keys { impl Display for Claims { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Email: {}\nCompany: {}", self.sub, self.company) + write!(f, "Email: {}", self.sub) } } diff --git a/src/model/db_model.rs b/src/model/db_model.rs index 17123cd..c298749 100644 --- a/src/model/db_model.rs +++ b/src/model/db_model.rs @@ -19,3 +19,40 @@ pub struct CategoryForm { pub uid: i64, pub name: String, } + +#[derive(Queryable, Selectable, serde::Serialize, serde::Deserialize)] +#[diesel(table_name = schema::tags)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Tag { + id: i64, + uid: i64, + name: String, + is_delete: bool, + create_at: chrono::NaiveDateTime, + update_at: chrono::NaiveDateTime, +} + +#[derive(serde::Deserialize, Insertable)] +#[diesel(table_name = schema::tags)] +pub struct TagForm { + pub uid: i64, + pub name: String, +} + +#[derive(Queryable, Selectable, serde::Serialize)] +#[diesel(table_name = schema::users)] +pub struct User { + pub id: i64, + pub username: String, + pub password: String, + pub mail: String, + pub is_delete: bool, +} + +#[derive(Insertable)] +#[diesel(table_name = schema::users)] +pub struct UserForm { + pub username: String, + pub password: String, + pub mail: String, +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 37c9ce0..81dfddf 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,2 +1,3 @@ pub mod db_model; pub mod schema; +pub mod req; diff --git a/src/model/req.rs b/src/model/req.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/user/dal.rs b/src/user/dal.rs new file mode 100644 index 0000000..228d6a5 --- /dev/null +++ b/src/user/dal.rs @@ -0,0 +1,118 @@ +use diesel::prelude::*; +use crate::model::{db_model, schema}; +use std::error::Error; +use std::fmt::Debug; +use pbkdf2::password_hash::{PasswordHash, PasswordVerifier}; +use pbkdf2::Pbkdf2; +use serde_json::json; + +pub async fn add_user(app_state: crate::AppState, username: String, password: String, mail: String) -> Result<(), ()> { + let conn = app_state + .db + .get() + .await + .map_err(|_| { + println!("fail to get db connection"); + () + })?; + let target_username = username.clone(); + // 1. check if current username exists. + let res = conn.interact( + move |conn| { + schema::users::table + .filter(schema::users::username.eq(target_username.clone())) + .count() + .get_result::(conn) + }) + .await + .map_err(|res| { + () + })? + .map_err(|res| { + () + })?; + println!("ret {}", res); + if res > 0 { + println!("user already exists."); + return Ok(()); + } + let new_user_form = db_model::UserForm { + username: username.clone(), + password: password.clone(), + mail: mail.clone(), + }; + // 2. adding user + let add_res = conn.interact( + move |conn| { + diesel::insert_into(schema::users::table) + .values(&new_user_form) + .returning(db_model::User::as_returning()) + .get_result(conn) + }) + .await + .map_err(|e| { + () + })? + .map_err(|e| { + () + })?; + let out = json!(add_res); + println!("new user {}", out.to_string()); + Ok(()) +} + +pub async fn check_user_psw(app_state: crate::AppState, username: String, password: String) -> bool { + let conn_res = app_state + .db + .get() + .await + .map_err(|_| { + println!("fail to get db connection"); + () + }); + let conn = match conn_res { + Ok(res) => res, + Err(err) => { return false; } + }; + // 1. get psw hash + let query_username = username.clone(); + let user_rr = conn.interact( + |conn| { + schema::users::table + .filter(schema::users::username.eq(query_username)) + .select(db_model::User::as_select()) + .get_results(conn) + }) + .await; + + let user_res = match user_rr { + Ok(res) => res, + Err(_) => return false, + }; + println!("get user_res success"); + let user = match user_res { + Ok(u) => u, + Err(_) => return false, + }; + + println!("get user success"); + + if user.len() != 1 { + return false; + } + println!("get uniq user success"); + let cur_user = user.get(0); + let psw = match cur_user { + Some(usr) => usr.password.clone(), + None => "".to_string(), + }; + println!("comparing psw, get {}, stored {}.", password.clone(), psw.clone()); + + let hash_res = PasswordHash::new(psw.as_str()); + let hash = match hash_res { + Ok(rs) => rs, + Err(_) => return false, + }; + let check_res = Pbkdf2.verify_password(password.as_bytes(), &hash); + return check_res.is_ok(); +} diff --git a/src/user/handler.rs b/src/user/handler.rs new file mode 100644 index 0000000..510297e --- /dev/null +++ b/src/user/handler.rs @@ -0,0 +1,29 @@ +use axum::{ + extract::State, http::StatusCode, routing::post, Json, Router +}; +use axum_macros::debug_handler; +use crate::middleware::auth::Claims; +use super::dal::check_user_psw; + +pub fn get_nest_handlers() -> Router { + Router::new() + .route("/login", post(login)) +} + +#[derive(serde::Deserialize)] +pub struct LoginCredentialRequest { + pub username: String, + pub password: String, +} + +#[debug_handler] +pub async fn login( + State(app_state): State, + Json(payload): Json, +) -> Result<(), (StatusCode, String)> { + let res = check_user_psw(app_state, payload.username.clone(), payload.password.clone()).await; + if !res { + return Err((StatusCode::UNAUTHORIZED, "invalid credentials".to_string())); + } + Ok(()) +} diff --git a/src/category/mod.rs b/src/user/mod.rs similarity index 56% rename from src/category/mod.rs rename to src/user/mod.rs index 062ae9d..5216b28 100644 --- a/src/category/mod.rs +++ b/src/user/mod.rs @@ -1 +1,2 @@ +pub mod dal; pub mod handler; diff --git a/src/util/mod.rs b/src/util/mod.rs index 1b7e472..13815ca 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1 +1,2 @@ pub mod req; +pub mod pass; diff --git a/src/util/pass.rs b/src/util/pass.rs new file mode 100644 index 0000000..305f007 --- /dev/null +++ b/src/util/pass.rs @@ -0,0 +1,16 @@ +use std::error::Error; +use pbkdf2::{ + password_hash::{ + rand_core::OsRng, + PasswordHash,SaltString, + }, + Pbkdf2, +}; +use pbkdf2::password_hash::PasswordHasher; + +pub fn get_pbkdf2_from_psw(password:String) -> Result { + let salt = SaltString::generate(&mut OsRng); + let password_hash = Pbkdf2.hash_password(password.as_bytes(), &salt)?.to_string(); + println!("{}",password_hash); + return Ok(password_hash) +}