diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2fb6565..3056840 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,8 +41,8 @@ jobs: username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} port: ${{ secrets.VPS_PORT }} - source: ./target/release/imphnen-backend-service - target: /opt/imphnen-backend-service/imphnen-backend-service + source: ./target/release/najm-course-api + target: /opt/najm-course-api/najm-course-api rm: true overwrite: true diff --git a/Cargo.lock b/Cargo.lock index c3d03ef..dc76ac5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "async-graphql" -version = "7.0.15" +version = "7.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfff2b17d272a5e3e201feda444e2c24b011fa722951268d1bd8b9b5bc6dc449" +checksum = "d3ee559e72d983e7e04001ba3bf32e6b71c1d670595780723727fd8a29d36e87" dependencies = [ "async-graphql-derive", "async-graphql-parser", @@ -238,9 +238,9 @@ dependencies = [ [[package]] name = "async-graphql-derive" -version = "7.0.15" +version = "7.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e5d0c6697def2f79ccbd972fb106b633173a6066e430b480e1ff9376a7561a" +checksum = "29db05b624fb6352fc11bfe30c54ab1b16a1fe937d7c05a783f4e88ef1292b3b" dependencies = [ "Inflector", "async-graphql-parser", @@ -255,9 +255,9 @@ dependencies = [ [[package]] name = "async-graphql-parser" -version = "7.0.15" +version = "7.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8531ee6d292c26df31c18c565ff22371e7bdfffe7f5e62b69537db0b8fd554dc" +checksum = "4904895044116aab098ca82c6cec831ec43ed99efd04db9b70a390419bc88c5b" dependencies = [ "async-graphql-value", "pest", @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "async-graphql-value" -version = "7.0.15" +version = "7.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "741110dda927420a28fbc1c310543d3416f789a6ba96859c2c265843a0a96887" +checksum = "d0cde74de18e3a00c5dd5cfa002ab6f532e1a06c2a79ee6671e2fc353b400b92" dependencies = [ "bytes", "indexmap 2.8.0", @@ -388,29 +388,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-surrealdb-boilerplate" -version = "0.1.0" -dependencies = [ - "anyhow", - "argon2", - "axum", - "chrono", - "jsonwebtoken", - "lettre", - "log", - "rand 0.9.0", - "redis", - "serde", - "serde_json", - "surrealdb", - "thiserror 2.0.12", - "tokio", - "tower-http", - "utoipa", - "utoipa-swagger-ui", -] - [[package]] name = "backtrace" version = "0.3.74" @@ -2148,6 +2125,29 @@ dependencies = [ "version_check", ] +[[package]] +name = "najm-course-api" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "axum", + "chrono", + "jsonwebtoken", + "lettre", + "log", + "rand 0.9.0", + "redis", + "serde", + "serde_json", + "surrealdb", + "thiserror 2.0.12", + "tokio", + "tower-http", + "utoipa", + "utoipa-swagger-ui", +] + [[package]] name = "nanoid" version = "0.4.0" @@ -3478,15 +3478,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -3885,9 +3876,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", "getrandom 0.3.2", @@ -4043,9 +4034,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", diff --git a/Cargo.toml b/Cargo.toml index 4ea2f5a..d185800 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "axum-surrealdb-boilerplate" +name = "najm-course-api" version = "0.1.0" edition = "2021" @@ -8,7 +8,7 @@ axum = { version = "0.8.1", features = ["multipart"] } log = "0.4.25" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" -tokio = { version = "1.43.0", features = ["full"] } +tokio = { version = "1.43.0" } argon2 = { version = "0.5.3", features = ["password-hash"] } jsonwebtoken = "9.3.1" chrono = "0.4.39" diff --git a/src/apps/mod.rs b/src/apps/mod.rs index d23cb3e..be1600f 100644 --- a/src/apps/mod.rs +++ b/src/apps/mod.rs @@ -9,6 +9,8 @@ use utoipa_swagger_ui::SwaggerUi; pub mod v1; pub mod v2; +pub use v1::*; + pub async fn apps(surrealdb: SurrealClient, redisdb: RedisClient) -> Router { let state = AppState { surrealdb, redisdb }; let env = Env::new(); diff --git a/src/apps/v1/auth/auth_dto.rs b/src/apps/v1/auth/auth_dto.rs index f2e8363..3a50ac9 100644 --- a/src/apps/v1/auth/auth_dto.rs +++ b/src/apps/v1/auth/auth_dto.rs @@ -1,8 +1,7 @@ +use crate::UsersItemDto; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::v1::UsersItemDto; - #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct AuthLoginRequestDto { pub email: String, diff --git a/src/apps/v1/auth/auth_middleware.rs b/src/apps/v1/auth/auth_middleware.rs index 848a4fc..4d59b86 100644 --- a/src/apps/v1/auth/auth_middleware.rs +++ b/src/apps/v1/auth/auth_middleware.rs @@ -1,6 +1,6 @@ -use super::AuthRepository; use crate::{ common_response, extract_email, v1::users_schema::UsersSchema, AppState, + UsersRepository, }; use axum::{ extract::Request, http::StatusCode, middleware::Next, response::Response, @@ -25,7 +25,7 @@ pub async fn auth_middleware( } }; - let repository = AuthRepository::new(&state); + let repository = UsersRepository::new(&state); let user: Option = match repository.query_user_by_email(email).await { Ok(user) => Some(user), diff --git a/src/apps/v1/auth/auth_repository.rs b/src/apps/v1/auth/auth_repository.rs index 152e5e7..8b01151 100644 --- a/src/apps/v1/auth/auth_repository.rs +++ b/src/apps/v1/auth/auth_repository.rs @@ -1,10 +1,4 @@ -use super::{ - AuthActiveInactiveRequestDto, AuthRegisterRequestDto, AuthSetNewPasswordRequestDto, -}; -use crate::{ - v1::{users_schema::UsersSchema, UsersItemDto}, - AppState, RedisKeyEnum, ResourceEnum, -}; +use crate::{AppState, RedisKeyEnum, UsersSchema}; use anyhow::{anyhow, bail, Result}; use redis::Commands; @@ -17,10 +11,7 @@ impl<'a> AuthRepository<'a> { Self { state } } - pub fn query_store_user_data( - &self, - user: AuthRegisterRequestDto, - ) -> Result { + pub fn query_store_user_data(&self, user: UsersSchema) -> Result { let redis_key = format!("{}:{}", RedisKeyEnum::User, user.email.clone()); match &self.state.redisdb.get_connection().and_then(|mut conn| { conn.set_ex::<_, String, ()>( @@ -34,13 +25,13 @@ impl<'a> AuthRepository<'a> { } } - pub fn query_get_stored_user(&self, email: String) -> Result { + pub fn query_get_stored_user(&self, email: String) -> Result { let redis_key = format!("{}:{}", RedisKeyEnum::User, email); let mut conn = self.state.redisdb.get_connection()?; let data: Option = conn.get(&redis_key)?; match data { Some(user_json) => { - let user: UsersItemDto = serde_json::from_str(&user_json)?; + let user: UsersSchema = serde_json::from_str(&user_json)?; Ok(user) } None => bail!("No stored user data found"), @@ -92,73 +83,4 @@ impl<'a> AuthRepository<'a> { Err(e) => Err(anyhow!("Failed to delete OTP from Redis: {}", e)), } } - - pub async fn query_user_by_email(&self, email: String) -> Result { - let db = &self.state.surrealdb; - let result = db - .select((ResourceEnum::Users.to_string(), email.clone())) - .await?; - match result { - Some(response) => Ok(response), - None => { - bail!("User not found") - } - } - } - - pub async fn query_create_user( - &self, - data: AuthRegisterRequestDto, - ) -> Result { - let db = &self.state.surrealdb; - let record: Option = db - .create((ResourceEnum::Users.to_string(), &data.email)) - .content(UsersSchema { - fullname: data.fullname.clone(), - email: data.email.clone(), - password: data.password.clone(), - is_active: false, - }) - .await?; - match record { - Some(_) => Ok("Success create user".into()), - None => bail!("Failed to create user"), - } - } - - pub async fn query_active_inactive_user( - &self, - data: AuthActiveInactiveRequestDto, - ) -> Result { - let db = &self.state.surrealdb; - let record: Option = db - .update((ResourceEnum::Users.to_string(), &data.email)) - .merge(AuthActiveInactiveRequestDto { - email: data.email.clone(), - is_active: data.is_active.clone(), - }) - .await?; - match record { - Some(_) => Ok("Success update user".into()), - None => bail!("Failed to update user"), - } - } - - pub async fn query_update_password_user( - &self, - data: AuthSetNewPasswordRequestDto, - ) -> Result { - let db = &self.state.surrealdb; - let record: Option = db - .update((ResourceEnum::Users.to_string(), &data.email)) - .merge(AuthSetNewPasswordRequestDto { - email: data.email.clone(), - password: data.password.clone(), - }) - .await?; - match record { - Some(_) => Ok("Success update password user".into()), - None => bail!("Failed to update password user"), - } - } } diff --git a/src/apps/v1/auth/auth_service.rs b/src/apps/v1/auth/auth_service.rs index c1981a0..5a22c48 100644 --- a/src/apps/v1/auth/auth_service.rs +++ b/src/apps/v1/auth/auth_service.rs @@ -1,15 +1,14 @@ -use axum::{http::StatusCode, response::Response}; - use super::{ - AuthActiveInactiveRequestDto, AuthLoginRequestDto, AuthLoginResponsetDto, - AuthRegisterRequestDto, AuthRepository, AuthResendOtpRequestDto, - AuthVerifyEmailRequestDto, TokenDto, + AuthLoginRequestDto, AuthLoginResponsetDto, AuthRegisterRequestDto, + AuthRepository, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, TokenDto, }; use crate::{ common_response, encode_access_token, encode_refresh_token, generate_otp, - hash_password, send_email, success_response, v1::UsersItemDto, verify_password, - AppState, Env, ResponseSuccessDto, + hash_password, send_email, success_response, verify_password, AppState, Env, + ResponseSuccessDto, UsersActiveInactiveSchema, UsersItemDto, UsersRepository, + UsersSchema, }; +use axum::{http::StatusCode, response::Response}; pub struct AuthService; @@ -18,9 +17,10 @@ impl AuthService { payload: AuthLoginRequestDto, state: &AppState, ) -> Response { - let repository = AuthRepository::new(state); + let user_repo = UsersRepository::new(state); + let auth_repo = AuthRepository::new(state); - match repository.query_user_by_email(payload.email.clone()).await { + match user_repo.query_user_by_email(payload.email.clone()).await { Ok(user) => { let is_password_correct = verify_password(&payload.password, &user.password).unwrap_or(false); @@ -73,11 +73,7 @@ impl AuthService { }, }; - if let Err(_) = repository.query_store_user_data(AuthRegisterRequestDto { - fullname: user.fullname, - password: user.password, - email: user.email, - }) { + if let Err(_) = auth_repo.query_store_user_data(user) { return common_response(StatusCode::BAD_REQUEST, "Failed to store data"); } @@ -91,8 +87,10 @@ impl AuthService { payload: AuthRegisterRequestDto, state: &AppState, ) -> Response { - let repository = AuthRepository::new(state); - if repository + let user_repo = UsersRepository::new(state); + let auth_repo = AuthRepository::new(state); + + if user_repo .query_user_by_email(payload.email.clone()) .await .is_ok() @@ -118,7 +116,7 @@ impl AuthService { let otp = generate_otp::OtpManager::generate_otp(); - repository + auth_repo .query_store_otp(new_user.email.clone(), otp.clone()) .unwrap(); @@ -126,7 +124,15 @@ impl AuthService { send_email(&new_user.email.clone(), "OTP Verification", &message).unwrap(); - match repository.query_create_user(new_user).await { + match user_repo + .query_create_user(UsersSchema { + email: new_user.email.clone(), + fullname: new_user.fullname.clone(), + password: new_user.password.clone(), + is_active: false, + }) + .await + { Ok(_) => common_response(StatusCode::CREATED, "Registration successful"), Err(err) => { common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()) @@ -154,8 +160,8 @@ impl AuthService { payload: AuthResendOtpRequestDto, state: &AppState, ) -> Response { - let repository = AuthRepository::new(state); - if repository + let user_repo = UsersRepository::new(state); + if user_repo .query_user_by_email(payload.email.clone()) .await .is_err() @@ -188,14 +194,16 @@ impl AuthService { payload: AuthVerifyEmailRequestDto, state: &AppState, ) -> Response { - let repository = AuthRepository::new(state); - match repository.query_get_stored_otp(payload.email.clone()) { + let user_repo = UsersRepository::new(state); + let auth_repo = AuthRepository::new(state); + + match auth_repo.query_get_stored_otp(payload.email.clone()) { Ok(stored_otp) => { let user_otp = payload.otp; let is_otp_valid = stored_otp == user_otp; if is_otp_valid { - match repository - .query_active_inactive_user(AuthActiveInactiveRequestDto { + match user_repo + .query_active_inactive_user(UsersActiveInactiveSchema { email: payload.email.clone(), is_active: true, }) @@ -203,7 +211,7 @@ impl AuthService { { Ok(_) => { if let Err(e) = - repository.query_delete_stored_otp(payload.email.clone()) + auth_repo.query_delete_stored_otp(payload.email.clone()) { return common_response( StatusCode::INTERNAL_SERVER_ERROR, @@ -215,7 +223,7 @@ impl AuthService { Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()), } } else { - if let Err(e) = repository.query_delete_stored_otp(payload.email.clone()) { + if let Err(e) = auth_repo.query_delete_stored_otp(payload.email.clone()) { return common_response( StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to delete OTP: {}", e), diff --git a/src/apps/v1/users/mod.rs b/src/apps/v1/users/mod.rs index 169dbdc..f26adcf 100644 --- a/src/apps/v1/users/mod.rs +++ b/src/apps/v1/users/mod.rs @@ -1,4 +1,7 @@ pub mod users_dto; +pub mod users_repository; pub mod users_schema; pub use users_dto::*; +pub use users_repository::*; +pub use users_schema::*; diff --git a/src/apps/v1/users/users_dto.rs b/src/apps/v1/users/users_dto.rs index 9badfb9..a85a25a 100644 --- a/src/apps/v1/users/users_dto.rs +++ b/src/apps/v1/users/users_dto.rs @@ -1,6 +1,13 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct UsersCreateRequestDto { + pub email: String, + pub fullname: String, + pub password: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct UsersItemDto { pub email: String, diff --git a/src/apps/v1/users/users_repository.rs b/src/apps/v1/users/users_repository.rs new file mode 100644 index 0000000..73f37ff --- /dev/null +++ b/src/apps/v1/users/users_repository.rs @@ -0,0 +1,79 @@ +use super::{UsersActiveInactiveSchema, UsersSetNewPasswordSchema}; +use crate::{v1::users_schema::UsersSchema, AppState, ResourceEnum}; +use anyhow::{bail, Result}; + +pub struct UsersRepository<'a> { + state: &'a AppState, +} + +impl<'a> UsersRepository<'a> { + pub fn new(state: &'a AppState) -> Self { + Self { state } + } + + pub async fn query_user_by_email(&self, email: String) -> Result { + let db = &self.state.surrealdb; + let result = db + .select((ResourceEnum::Users.to_string(), email.clone())) + .await?; + match result { + Some(response) => Ok(response), + None => { + bail!("User not found") + } + } + } + + pub async fn query_create_user(&self, data: UsersSchema) -> Result { + let db = &self.state.surrealdb; + let record: Option = db + .create((ResourceEnum::Users.to_string(), &data.email)) + .content(UsersSchema { + fullname: data.fullname.clone(), + email: data.email.clone(), + password: data.password.clone(), + is_active: false, + }) + .await?; + match record { + Some(_) => Ok("Success create user".into()), + None => bail!("Failed to create user"), + } + } + + pub async fn query_active_inactive_user( + &self, + data: UsersActiveInactiveSchema, + ) -> Result { + let db = &self.state.surrealdb; + let record: Option = db + .update((ResourceEnum::Users.to_string(), &data.email)) + .merge(UsersActiveInactiveSchema { + email: data.email.clone(), + is_active: data.is_active.clone(), + }) + .await?; + match record { + Some(_) => Ok("Success update user".into()), + None => bail!("Failed to update user"), + } + } + + pub async fn query_update_password_user( + &self, + data: UsersSetNewPasswordSchema, + ) -> Result { + let db = &self.state.surrealdb; + let record: Option = db + .update((ResourceEnum::Users.to_string(), &data.email)) + .merge(UsersSetNewPasswordSchema { + email: data.email.clone(), + password: data.password.clone(), + }) + .await?; + match record { + Some(_) => Ok("Success update password user".into()), + None => bail!("Failed to update password user"), + } + } +} diff --git a/src/apps/v1/users/users_schema.rs b/src/apps/v1/users/users_schema.rs index 35f34e5..eff6635 100644 --- a/src/apps/v1/users/users_schema.rs +++ b/src/apps/v1/users/users_schema.rs @@ -1,10 +1,21 @@ use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct UsersSchema { pub email: String, pub fullname: String, pub password: String, pub is_active: bool, } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct UsersSetNewPasswordSchema { + pub email: String, + pub password: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct UsersActiveInactiveSchema { + pub email: String, + pub is_active: bool, +} diff --git a/src/main.rs b/src/main.rs index ca3b8db..f0335b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use axum_surrealdb_boilerplate::{apps, axum_init}; +use najm_course_api::{apps, axum_init}; #[tokio::main] async fn main() {