feat: auth

This commit is contained in:
Maulana Sodiqin
2025-03-13 23:40:16 +07:00
parent 298144bd36
commit acfb8259bc
17 changed files with 226 additions and 179 deletions
+1 -1
View File
@@ -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]
-1
View File
@@ -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)
+11 -3
View File
@@ -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<AppState>,
@@ -9,3 +10,10 @@ pub async fn post_login(
) -> impl IntoResponse {
mutation_login(payload, &state).await
}
pub async fn post_register(
Extension(state): Extension<AppState>,
Json(payload): Json<AuthRegisterRequestDto>,
) -> impl IntoResponse {
mutation_register(payload, &state).await
}
+8 -4
View File
@@ -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)]
+20 -95
View File
@@ -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<AuthLoginRequestDto> = 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<UsersItemDto, Box<dyn Error>> {
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<UsersItemDto> = 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<String, Box<dyn Error>> {
let db = &state.surrealdb;
let existing_user: Option<UsersItemDto> =
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<UsersItemDto> =
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(&params.password);
let created_user: AuthLoginRequestDto =
match state.surrealdb.create(&user_key, &params).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())
}
+64
View File
@@ -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())
}
}
}
+5 -1
View File
@@ -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))
}
+1
View File
@@ -5,4 +5,5 @@ use utoipa::ToSchema;
pub struct UsersItemDto {
pub email: String,
pub fullname: String,
pub password: String,
}
+1 -1
View File
@@ -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)]
+6 -15
View File
@@ -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<F, Fut>(router_fn: F)
where
F: FnOnce(SurrealClient, RedisClient) -> Fut,
Fut: Future<Output = Router>,
{
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),
+57
View File
@@ -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()),
}
}
}
+20 -26
View File
@@ -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<String, StatusCode> {
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<String, StatusCode> {
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<String, StatusCode> {
pub fn decode_access_token(
jwt_token: &str,
) -> Result<TokenData<Claims>, 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<TokenData<Claims>, 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<String, StatusCode> {
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<String, StatusCode> {
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<String, StatusCode> {
pub fn decode_refresh_token(
jwt_token: &str,
) -> Result<TokenData<Claims>, 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<TokenData<Claims>, StatusCode> = decode(
&jwt_token,
&DecodingKey::from_secret(secret.as_ref()),
+9 -10
View File
@@ -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<dyn std::error::Error>> {
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<dyn Error>> {
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);
+4 -2
View File
@@ -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::*;
-16
View File
@@ -1,16 +0,0 @@
use redis::Client;
use redis::RedisResult;
use std::env;
pub async fn redisdb_init() -> RedisResult<Client> {
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)
}
+11
View File
@@ -0,0 +1,11 @@
use super::Env;
use redis::Client;
use redis::RedisResult;
pub async fn redisdb_init() -> RedisResult<Client> {
let env = Env::new();
let host_name = env.redis_hostname;
let url = format!("redis://{}", host_name);
let client = Client::open(url)?;
Ok(client)
}
+8 -4
View File
@@ -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<SurrealClient> {
let db = Surreal::<Client>::init();
db.connect::<Ws>("ws://localhost:8000").await?;
db.connect::<Http>("localhost:8000").await?;
db.signin(surrealdb::opt::auth::Root {
username: "root",
password: "root",
})
.await?;
db.use_ns("test").use_db("test").await?;
Ok(db)
}