From acfb8259bcaba38d8a24aae4bfe85b35f71bbd28 Mon Sep 17 00:00:00 2001 From: Maulana Sodiqin Date: Thu, 13 Mar 2025 23:40:16 +0700 Subject: [PATCH] feat: auth --- Cargo.toml | 2 +- src/apps/mod.rs | 1 - src/apps/v1/auth/auth_controller.rs | 14 +++- src/apps/v1/auth/auth_dto.rs | 12 ++- src/apps/v1/auth/auth_repository.rs | 115 +++++----------------------- src/apps/v1/auth/auth_service.rs | 64 ++++++++++++++++ src/apps/v1/auth/mod.rs | 6 +- src/apps/v1/users/users_dto.rs | 1 + src/entities/common_dto.rs | 2 +- src/libs/axum/mod.rs | 21 ++--- src/libs/enviroment/mod.rs | 57 ++++++++++++++ src/libs/jsonwebtoken/mod.rs | 46 +++++------ src/libs/lettre/mod.rs | 19 +++-- src/libs/mod.rs | 6 +- src/libs/redis/mod.rs | 16 ---- src/libs/redisdb/mod.rs | 11 +++ src/libs/surrealdb/mod.rs | 12 ++- 17 files changed, 226 insertions(+), 179 deletions(-) create mode 100644 src/apps/v1/auth/auth_service.rs create mode 100644 src/libs/enviroment/mod.rs delete mode 100644 src/libs/redis/mod.rs create mode 100644 src/libs/redisdb/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 2cbd686..77e0a16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ utoipa = { version = "5.3.1", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] } redis = "0.28.2" lettre = { version = "0.11.12", features = ["tokio1-native-tls"] } -surrealdb = "2.2.1" +surrealdb = { version = "2.2.1", features = ["protocol-http"] } thiserror = "2.0.11" [profile.release] diff --git a/src/apps/mod.rs b/src/apps/mod.rs index 2b7ea5d..e49a135 100644 --- a/src/apps/mod.rs +++ b/src/apps/mod.rs @@ -6,7 +6,6 @@ pub mod v2; pub async fn apps(surrealdb: SurrealClient, redisdb: RedisClient) -> Router { let state = AppState { surrealdb, redisdb }; - Router::new() .nest("/v1", v1::routes().await) .nest("/v2", v2::routes().await) diff --git a/src/apps/v1/auth/auth_controller.rs b/src/apps/v1/auth/auth_controller.rs index 61318cd..8ae6f28 100644 --- a/src/apps/v1/auth/auth_controller.rs +++ b/src/apps/v1/auth/auth_controller.rs @@ -1,7 +1,8 @@ -use axum::{response::IntoResponse, Extension, Json}; - -use super::{mutation_login, AuthLoginRequestDto}; +use super::{ + mutation_login, mutation_register, AuthLoginRequestDto, AuthRegisterRequestDto, +}; use crate::AppState; +use axum::{response::IntoResponse, Extension, Json}; pub async fn post_login( Extension(state): Extension, @@ -9,3 +10,10 @@ pub async fn post_login( ) -> impl IntoResponse { mutation_login(payload, &state).await } + +pub async fn post_register( + Extension(state): Extension, + Json(payload): Json, +) -> impl IntoResponse { + mutation_register(payload, &state).await +} diff --git a/src/apps/v1/auth/auth_dto.rs b/src/apps/v1/auth/auth_dto.rs index e77ab8f..d3790f8 100644 --- a/src/apps/v1/auth/auth_dto.rs +++ b/src/apps/v1/auth/auth_dto.rs @@ -8,10 +8,14 @@ pub struct AuthLoginRequestDto { } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] -pub struct AuthRegisterRequestDto { - pub email: String, - pub password: String, - pub fullname: String, +pub struct AuthLoginResponsetDto { + pub token: TokenDto, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct TokenDto { + pub access_token: String, + pub refresh_token: String, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] diff --git a/src/apps/v1/auth/auth_repository.rs b/src/apps/v1/auth/auth_repository.rs index aafeb2e..e67b4dc 100644 --- a/src/apps/v1/auth/auth_repository.rs +++ b/src/apps/v1/auth/auth_repository.rs @@ -1,107 +1,32 @@ -use super::{auth_dto::AuthLoginRequestDto, AuthRegisterRequestDto}; -use crate::{ - common_response, hash_password, success_response, v1::UsersItemDto, AppState, - ResponseSuccessDto, -}; -use axum::{http::StatusCode, response::Response}; -use serde_json; +use crate::{v1::UsersItemDto, AppState}; +use std::error::Error; -const USERS_KEY: &str = "users"; +use super::AuthRegisterRequestDto; -pub async fn mutation_login( - params: AuthLoginRequestDto, +pub async fn query_user_by_email( + email: String, state: &AppState, -) -> Response { - let user: Option = match state - .surrealdb - .select((USERS_KEY, params.email.as_str())) - .await - { - Ok(user) => user, - Err(err) => { - return common_response( - StatusCode::INTERNAL_SERVER_ERROR, - &err.to_string(), - ); - } - }; +) -> Result> { + let db = &state.surrealdb; - let mut redis_conn = match state.redisdb.get_connection() { - Ok(conn) => conn, - Err(err) => { - return common_response( - StatusCode::INTERNAL_SERVER_ERROR, - &err.to_string(), - ); - } - }; + let mut result = db + .query("SELECT * FROM app_users WHERE email = $email LIMIT 1;") + .bind(("email", email.clone())) + .await?; - let user_json = match serde_json::to_string(&user) { - Ok(json) => json, - Err(err) => { - return common_response( - StatusCode::INTERNAL_SERVER_ERROR, - &err.to_string(), - ); - } - }; + let user: Option = result.take(0)?; - if let Err(err) = redis::cmd("SET") - .arg("users_data") - .arg(user_json) - .query::<()>(&mut redis_conn) - { - return common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()); - } - - let response = ResponseSuccessDto { data: params }; - - success_response(response) + user.ok_or_else(|| format!("User not found for email: {}", email).into()) } -pub async fn mutation_register( - params: AuthRegisterRequestDto, +pub async fn query_create_user( + data: AuthRegisterRequestDto, state: &AppState, -) -> Response { - let user_key = format!("{}:{}", USERS_KEY, params.email); +) -> Result> { + let db = &state.surrealdb; - let existing_user: Option = - match state.surrealdb.select(&user_key).await { - Ok(user) => user, - Err(err) => { - return common_response( - StatusCode::INTERNAL_SERVER_ERROR, - &err.to_string(), - ); - } - }; + let _record: Option = + db.create(("app_users", &data.email)).content(data).await?; - if existing_user.is_some() { - return common_response(StatusCode::CONFLICT, "User already exists"); - } - - let hashed_password = hash_password(¶ms.password); - - let created_user: AuthLoginRequestDto = - match state.surrealdb.create(&user_key, ¶ms).await { - Ok(user) => Some(user), - Err(err) => { - return common_response( - StatusCode::INTERNAL_SERVER_ERROR, - &err.to_string(), - ); - } - }; - - let user_json = match serde_json::to_string(&created_user) { - Ok(json) => json, - Err(err) => { - return common_response( - StatusCode::INTERNAL_SERVER_ERROR, - &err.to_string(), - ); - } - }; - - common_response(StatusCode::CREATED, "Success Register User") + Ok("Success create user".into()) } diff --git a/src/apps/v1/auth/auth_service.rs b/src/apps/v1/auth/auth_service.rs new file mode 100644 index 0000000..87a8cca --- /dev/null +++ b/src/apps/v1/auth/auth_service.rs @@ -0,0 +1,64 @@ +use axum::{http::StatusCode, response::Response}; + +use super::{ + query_create_user, query_user_by_email, AuthLoginRequestDto, + AuthRegisterRequestDto, +}; +use crate::{common_response, hash_password, verify_password, AppState}; + +pub async fn mutation_login( + payload: AuthLoginRequestDto, + state: &AppState, +) -> Response { + match query_user_by_email(payload.email, state).await { + Ok(user) => { + let is_password_correct = + verify_password(&payload.password, &user.password).unwrap_or(false); + + if is_password_correct { + common_response( + StatusCode::BAD_REQUEST, + "Email or password not correct", + ); + } + + common_response(StatusCode::OK, "Success Login") + } + Err(err) => common_response(StatusCode::UNAUTHORIZED, &err.to_string()), + } +} + +pub async fn mutation_register( + payload: AuthRegisterRequestDto, + state: &AppState, +) -> Response { + if query_user_by_email(payload.email.clone(), state) + .await + .is_ok() + { + return common_response(StatusCode::BAD_REQUEST, "User already exists"); + } + + let hashed_password = match hash_password(&payload.password) { + Ok(hash) => hash, + Err(_) => { + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to hash password", + ); + } + }; + + let new_user = AuthRegisterRequestDto { + email: payload.email, + password: hashed_password, + fullname: payload.fullname, + }; + + match query_create_user(new_user, state).await { + Ok(_) => common_response(StatusCode::CREATED, "Registration successful"), + Err(err) => { + common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()) + } + } +} diff --git a/src/apps/v1/auth/mod.rs b/src/apps/v1/auth/mod.rs index ab67c67..b189546 100644 --- a/src/apps/v1/auth/mod.rs +++ b/src/apps/v1/auth/mod.rs @@ -4,10 +4,14 @@ pub mod auth_controller; pub mod auth_dto; pub mod auth_middleware; pub mod auth_repository; +pub mod auth_service; pub use auth_dto::*; pub use auth_repository::*; +pub use auth_service::*; pub fn auth_router() -> Router { - Router::new().route("/login", post(auth_controller::post_login)) + Router::new() + .route("/login", post(auth_controller::post_login)) + .route("/register", post(auth_controller::post_register)) } diff --git a/src/apps/v1/users/users_dto.rs b/src/apps/v1/users/users_dto.rs index 3828896..d7e8bd0 100644 --- a/src/apps/v1/users/users_dto.rs +++ b/src/apps/v1/users/users_dto.rs @@ -5,4 +5,5 @@ use utoipa::ToSchema; pub struct UsersItemDto { pub email: String, pub fullname: String, + pub password: String, } diff --git a/src/entities/common_dto.rs b/src/entities/common_dto.rs index 1ccb16d..21658bc 100644 --- a/src/entities/common_dto.rs +++ b/src/entities/common_dto.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use surrealdb::{engine::remote::ws::Client, Surreal}; +use surrealdb::{engine::remote::http::Client, Surreal}; use utoipa::{IntoParams, ToSchema}; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] diff --git a/src/libs/axum/mod.rs b/src/libs/axum/mod.rs index ac02cea..6c6c122 100644 --- a/src/libs/axum/mod.rs +++ b/src/libs/axum/mod.rs @@ -1,31 +1,22 @@ +use crate::{redisdb_init, Env, RedisClient}; +use crate::{surrealdb_init, SurrealClient}; use axum::{serve, Router}; -use std::env; use std::{future::Future, net::SocketAddr}; use tokio::net::TcpListener; -use crate::{redisdb_init, RedisClient}; -use crate::{surrealdb_init, SurrealClient}; - pub async fn axum_init(router_fn: F) where F: FnOnce(SurrealClient, RedisClient) -> Fut, Fut: Future, { + let env = Env::new(); let surrealdb = surrealdb_init().await.expect("Failed surrealdb"); - let redisdb = redisdb_init().await.expect("Failed redisdb"); + let redisdb = redisdb_init().await.expect("Failed redisdb"); let router = router_fn(surrealdb, redisdb).await; - - let addr = SocketAddr::from(( - [0, 0, 0, 0], - env::var("PORT") - .unwrap_or_else(|_| "3000".to_string()) - .parse() - .unwrap(), - )); - + let port = env.port; + let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = TcpListener::bind(&addr).await.unwrap(); println!("Listening on http://{}", addr); - match serve(listener, router).await { Ok(_) => println!("Server stopped gracefully."), Err(err) => println!("Server encountered an error: {}", err), diff --git a/src/libs/enviroment/mod.rs b/src/libs/enviroment/mod.rs new file mode 100644 index 0000000..953c9be --- /dev/null +++ b/src/libs/enviroment/mod.rs @@ -0,0 +1,57 @@ +use std::env; + +pub struct Env { + pub port: u16, + pub access_token_secret: String, + pub refresh_token_secret: String, + pub database_url: String, + pub database_schema: String, + pub smtp_email: String, + pub smtp_password: String, + pub smtp_name: String, + pub smpt_host: String, + pub redis_hostname: String, + pub fe_url: String, + pub rust_env: String, + pub minio_endpoint: String, + pub minio_bucket_name: String, + pub minio_access_key: String, + pub minio_secret_key: String, +} + +impl Env { + pub fn new() -> Self { + Self { + port: env::var("PORT") + .unwrap_or("3000".to_string()) + .parse() + .unwrap_or(3000), + access_token_secret: env::var("ACCESS_TOKEN_SECRET") + .unwrap_or("default_access_secret".to_string()), + refresh_token_secret: env::var("REFRESH_TOKEN_SECRET") + .unwrap_or("default_refresh_secret".to_string()), + database_url: env::var("DATABASE_URL") + .unwrap_or("postgres://localhost".to_string()), + database_schema: env::var("DATABASE_SCHEMA") + .unwrap_or("public".to_string()), + smtp_email: env::var("SMTP_EMAIL") + .unwrap_or("no-reply@example.com".to_string()), + smtp_password: env::var("SMTP_PASSWORD") + .unwrap_or("default_smtp_password".to_string()), + smtp_name: env::var("SMTP_NAME").unwrap_or("MyApp SMTP".to_string()), + smpt_host: env::var("SMPT_HOST").unwrap_or("smpt.gmail.com".to_string()), + redis_hostname: env::var("REDIS_HOSTNAME") + .unwrap_or("localhost".to_string()), + fe_url: env::var("FE_URL").unwrap_or("http://localhost".to_string()), + rust_env: env::var("RUST_ENV").unwrap_or("development".to_string()), + minio_endpoint: env::var("MINIO_ENDPOINT") + .unwrap_or("http://localhost:9000".to_string()), + minio_bucket_name: env::var("MINIO_BUCKET_NAME") + .unwrap_or("default_bucket".to_string()), + minio_access_key: env::var("MINIO_ACCESS_KEY") + .unwrap_or("minio_access".to_string()), + minio_secret_key: env::var("MINIO_SECRET_KEY") + .unwrap_or("minio_secret".to_string()), + } + } +} diff --git a/src/libs/jsonwebtoken/mod.rs b/src/libs/jsonwebtoken/mod.rs index 290dafa..ac37c2a 100644 --- a/src/libs/jsonwebtoken/mod.rs +++ b/src/libs/jsonwebtoken/mod.rs @@ -1,31 +1,32 @@ +use super::Env; use axum::http::StatusCode; use chrono::{Duration, TimeDelta, Utc}; use jsonwebtoken::{ decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation, }; use serde::{Deserialize, Serialize}; -use std::env; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TokenSub { + pub email: String, + pub role_name: String, +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Claims { pub exp: usize, pub iat: usize, - pub email: String, + pub sub: TokenSub, } -pub fn encode_access_token(email: &str) -> Result { - let secret: String = env::var("ACCESS_TOKEN_SECRET") - .unwrap_or("this-is-secret".to_string()) - .to_string(); +pub fn encode_access_token(sub: TokenSub) -> Result { + let env = Env::new(); + let secret: String = env.access_token_secret; let now = Utc::now(); let expire: TimeDelta = Duration::minutes(15); let exp: usize = (now + expire).timestamp() as usize; let iat: usize = now.timestamp() as usize; - let claim = Claims { - iat, - exp, - email: email.to_string(), - }; + let claim = Claims { iat, exp, sub }; encode( &Header::default(), &claim, @@ -37,9 +38,8 @@ pub fn encode_access_token(email: &str) -> Result { pub fn decode_access_token( jwt_token: &str, ) -> Result, StatusCode> { - let secret = env::var("ACCESS_TOKEN_SECRET") - .unwrap_or("this-is-secret".to_string()) - .to_string(); + let env = Env::new(); + let secret: String = env.access_token_secret; let result: Result, StatusCode> = decode( &jwt_token, &DecodingKey::from_secret(secret.as_ref()), @@ -49,19 +49,14 @@ pub fn decode_access_token( result } -pub fn encode_refresh_token(email: &str) -> Result { - let secret: String = env::var("REFRESH_TOKEN_SECRET") - .unwrap_or("this-is-secret".to_string()) - .to_string(); +pub fn encode_refresh_token(sub: TokenSub) -> Result { + let env = Env::new(); + let secret: String = env.refresh_token_secret; let now = Utc::now(); let expire: TimeDelta = Duration::days(1); let exp: usize = (now + expire).timestamp() as usize; let iat: usize = now.timestamp() as usize; - let claim = Claims { - iat, - exp, - email: email.to_string(), - }; + let claim = Claims { iat, exp, sub }; encode( &Header::default(), &claim, @@ -73,9 +68,8 @@ pub fn encode_refresh_token(email: &str) -> Result { pub fn decode_refresh_token( jwt_token: &str, ) -> Result, StatusCode> { - let secret = env::var("REFRESH_TOKEN_SECRET") - .unwrap_or("this-is-secret".to_string()) - .to_string(); + let env = Env::new(); + let secret: String = env.refresh_token_secret; let result: Result, StatusCode> = decode( &jwt_token, &DecodingKey::from_secret(secret.as_ref()), diff --git a/src/libs/lettre/mod.rs b/src/libs/lettre/mod.rs index 1c5f2a9..90e31be 100644 --- a/src/libs/lettre/mod.rs +++ b/src/libs/lettre/mod.rs @@ -1,18 +1,20 @@ +use super::Env; use lettre::message::Mailbox; use lettre::transport::smtp::authentication::Credentials; use lettre::{Message, SmtpTransport, Transport}; -use std::env; +use std::error::Error; pub fn send_email( to: &str, subject: &str, body: &str, -) -> Result<(), Box> { - let sender_email = env::var("SMTP_EMAIL")?.to_string(); - let sender_name = env::var("SMTP_NAME")?.to_string(); - let sender_password = env::var("SMTP_PASSWORD")?.to_string(); +) -> Result<(), Box> { + let env = Env::new(); + let host = env.smpt_host; + let sender_email = env.smtp_email; + let sender_name = env.smtp_name; + let sender_password = env.smtp_password; let recipient_email = to; - let email = Message::builder() .from(Mailbox::new( Some(sender_name.replace("-", " ")), @@ -21,14 +23,11 @@ pub fn send_email( .to(recipient_email.parse()?) .subject(subject) .body(body.to_string())?; - let smtp_credentials = Credentials::new(sender_email, sender_password.replace("-", " ")); - - let mailer = SmtpTransport::relay("smtp.gmail.com")? + let mailer = SmtpTransport::relay(&host)? .credentials(smtp_credentials) .build(); - match mailer.send(&email) { Ok(_) => { println!("Email sent successfully to {}", to); diff --git a/src/libs/mod.rs b/src/libs/mod.rs index c943b53..ae30feb 100644 --- a/src/libs/mod.rs +++ b/src/libs/mod.rs @@ -1,13 +1,15 @@ pub mod argon; pub mod axum; +pub mod enviroment; pub mod jsonwebtoken; pub mod lettre; -pub mod redis; +pub mod redisdb; pub mod surrealdb; pub use argon::*; pub use axum::*; +pub use enviroment::*; pub use jsonwebtoken::*; pub use lettre::*; -pub use redis::*; +pub use redisdb::*; pub use surrealdb::*; diff --git a/src/libs/redis/mod.rs b/src/libs/redis/mod.rs deleted file mode 100644 index b8111da..0000000 --- a/src/libs/redis/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -use redis::Client; -use redis::RedisResult; -use std::env; - -pub async fn redisdb_init() -> RedisResult { - let host_name = - env::var("REDIS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); - let uri_scheme = if env::var("IS_TLS").is_ok() { - "rediss" - } else { - "redis" - }; - let url = format!("{}://{}", uri_scheme, host_name); - let client = Client::open(url)?; - Ok(client) -} diff --git a/src/libs/redisdb/mod.rs b/src/libs/redisdb/mod.rs new file mode 100644 index 0000000..5c18d75 --- /dev/null +++ b/src/libs/redisdb/mod.rs @@ -0,0 +1,11 @@ +use super::Env; +use redis::Client; +use redis::RedisResult; + +pub async fn redisdb_init() -> RedisResult { + let env = Env::new(); + let host_name = env.redis_hostname; + let url = format!("redis://{}", host_name); + let client = Client::open(url)?; + Ok(client) +} diff --git a/src/libs/surrealdb/mod.rs b/src/libs/surrealdb/mod.rs index d0c9e6a..8043026 100644 --- a/src/libs/surrealdb/mod.rs +++ b/src/libs/surrealdb/mod.rs @@ -1,11 +1,15 @@ -use surrealdb::engine::remote::ws::{Client, Ws}; -use surrealdb::{Result, Surreal}; - use crate::SurrealClient; +use surrealdb::engine::remote::http::{Client, Http}; +use surrealdb::{Result, Surreal}; pub async fn surrealdb_init() -> Result { let db = Surreal::::init(); - db.connect::("ws://localhost:8000").await?; + db.connect::("localhost:8000").await?; + db.signin(surrealdb::opt::auth::Root { + username: "root", + password: "root", + }) + .await?; db.use_ns("test").use_db("test").await?; Ok(db) }