feat: auth functionality
This commit is contained in:
Generated
+16
@@ -1765,12 +1765,14 @@ dependencies = [
|
|||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"lettre",
|
"lettre",
|
||||||
"log",
|
"log",
|
||||||
|
"rand 0.9.0",
|
||||||
"redis",
|
"redis",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"surrealdb",
|
"surrealdb",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower-http",
|
||||||
"utoipa",
|
"utoipa",
|
||||||
"utoipa-swagger-ui",
|
"utoipa-swagger-ui",
|
||||||
]
|
]
|
||||||
@@ -4143,6 +4145,20 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ lettre = { version = "0.11.12", features = ["tokio1-native-tls"] }
|
|||||||
surrealdb = { version = "2.2.1", features = ["protocol-http"] }
|
surrealdb = { version = "2.2.1", features = ["protocol-http"] }
|
||||||
thiserror = "2.0.11"
|
thiserror = "2.0.11"
|
||||||
anyhow = "1.0.97"
|
anyhow = "1.0.97"
|
||||||
|
rand = "0.9.0"
|
||||||
|
tower-http = { version = "0.6.2", features = ["cors"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
|
|||||||
+31
-2
@@ -1,5 +1,9 @@
|
|||||||
use crate::{AppState, RedisClient, SurrealClient};
|
use crate::{AppState, Env, RedisClient, SurrealClient};
|
||||||
use axum::{Extension, Router};
|
use axum::{
|
||||||
|
http::{header, HeaderValue, Method},
|
||||||
|
Extension, Router,
|
||||||
|
};
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
use utoipa_swagger_ui::SwaggerUi;
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
pub mod v1;
|
pub mod v1;
|
||||||
@@ -7,9 +11,34 @@ pub mod v2;
|
|||||||
|
|
||||||
pub async fn apps(surrealdb: SurrealClient, redisdb: RedisClient) -> Router {
|
pub async fn apps(surrealdb: SurrealClient, redisdb: RedisClient) -> Router {
|
||||||
let state = AppState { surrealdb, redisdb };
|
let state = AppState { surrealdb, redisdb };
|
||||||
|
let env = Env::new();
|
||||||
|
let cors_origins = match env.rust_env.as_str() {
|
||||||
|
"development" => vec!["http://localhost:5173"],
|
||||||
|
"production" => {
|
||||||
|
vec!["https://gacha.imphnen.dev", "https://imphnen.dev"]
|
||||||
|
}
|
||||||
|
_ => vec![
|
||||||
|
"http://localhost:5173",
|
||||||
|
"https://gacha.imphnen.dev",
|
||||||
|
"https://imphnen.dev",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let allowed_origins: Vec<HeaderValue> = cors_origins
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|origin| origin.parse::<HeaderValue>().ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let cors_middleware = CorsLayer::new()
|
||||||
|
.allow_origin(allowed_origins)
|
||||||
|
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
|
||||||
|
.allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE])
|
||||||
|
.allow_credentials(true);
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.nest("/v1", v1::routes().await)
|
.nest("/v1", v1::routes().await)
|
||||||
.nest("/v2", v2::routes().await)
|
.nest("/v2", v2::routes().await)
|
||||||
.merge(SwaggerUi::new("/docs").url("/openapi.json", v1::docs_router()))
|
.merge(SwaggerUi::new("/docs").url("/openapi.json", v1::docs_router()))
|
||||||
|
.layer(cors_middleware)
|
||||||
.layer(Extension(state))
|
.layer(Extension(state))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use super::{AuthLoginRequestDto, AuthRegisterRequestDto, AuthService};
|
use super::{
|
||||||
|
AuthLoginRequestDto, AuthRegisterRequestDto, AuthResendOtpRequestDto, AuthService,
|
||||||
|
AuthVerifyEmailRequestDto,
|
||||||
|
};
|
||||||
use crate::{v1::AuthLoginResponsetDto, AppState};
|
use crate::{v1::AuthLoginResponsetDto, AppState};
|
||||||
use axum::{response::IntoResponse, Extension, Json};
|
|
||||||
|
|
||||||
use crate::{MessageResponseDto, ResponseSuccessDto};
|
use crate::{MessageResponseDto, ResponseSuccessDto};
|
||||||
|
use axum::{response::IntoResponse, Extension, Json};
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
@@ -10,7 +12,7 @@ use crate::{MessageResponseDto, ResponseSuccessDto};
|
|||||||
request_body = AuthLoginRequestDto,
|
request_body = AuthLoginRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Login successful", body = ResponseSuccessDto<AuthLoginResponsetDto>),
|
(status = 200, description = "Login successful", body = ResponseSuccessDto<AuthLoginResponsetDto>),
|
||||||
(status = 401, description = "Unauthorized", body = MessageResponseDto)
|
(status = 401, description = "Login failed", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Authentication"
|
tag = "Authentication"
|
||||||
)]
|
)]
|
||||||
@@ -26,8 +28,8 @@ pub async fn post_login(
|
|||||||
path = "/v1/auth/register",
|
path = "/v1/auth/register",
|
||||||
request_body = AuthRegisterRequestDto,
|
request_body = AuthRegisterRequestDto,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Login successful", body = MessageResponseDto),
|
(status = 200, description = "Register successful", body = MessageResponseDto),
|
||||||
(status = 401, description = "Unauthorized", body = MessageResponseDto)
|
(status = 401, description = "Register failed", body = MessageResponseDto)
|
||||||
),
|
),
|
||||||
tag = "Authentication"
|
tag = "Authentication"
|
||||||
)]
|
)]
|
||||||
@@ -37,3 +39,54 @@ pub async fn post_register(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
AuthService::mutation_register(payload, &state).await
|
AuthService::mutation_register(payload, &state).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/verify",
|
||||||
|
request_body = AuthVerifyEmailRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Verify email successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Verify email failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_verify_email(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthVerifyEmailRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_verify_email(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/resend",
|
||||||
|
request_body = AuthResendOtpRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Resend otp successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Resend otp failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_resend_otp(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthResendOtpRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_resend_otp(payload, &state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/auth/forgot",
|
||||||
|
request_body = AuthResendOtpRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Forgot password request successful", body = MessageResponseDto),
|
||||||
|
(status = 401, description = "Forgot password request failed", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Authentication"
|
||||||
|
)]
|
||||||
|
pub async fn post_forgot_password(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<AuthResendOtpRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
AuthService::mutation_forgot_password(payload, &state).await
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,29 @@ pub struct AuthActiveInactiveRequestDto {
|
|||||||
pub email: String,
|
pub email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct AuthVerifyEmailRequestDto {
|
||||||
|
pub email: String,
|
||||||
|
pub otp: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct AuthResendOtpRequestDto {
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct AuthNewPasswordRequestDto {
|
||||||
|
pub token: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct AuthSetNewPasswordRequestDto {
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct AuthQueryByEmailResponseDto {
|
pub struct AuthQueryByEmailResponseDto {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use super::{AuthActiveInactiveRequestDto, AuthRegisterRequestDto};
|
use super::{
|
||||||
|
AuthActiveInactiveRequestDto, AuthRegisterRequestDto, AuthSetNewPasswordRequestDto,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
v1::{users_schema::UsersSchema, UsersItemDto},
|
v1::{users_schema::UsersSchema, UsersItemDto},
|
||||||
AppState, RedisKeyEnum, ResourceEnum,
|
AppState, RedisKeyEnum, ResourceEnum,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use redis::Commands;
|
use redis::Commands;
|
||||||
|
|
||||||
pub struct AuthRepository<'a> {
|
pub struct AuthRepository<'a> {
|
||||||
@@ -45,12 +47,62 @@ impl<'a> AuthRepository<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn query_get_stored_otp(&self, email: String) -> Result<u32> {
|
||||||
|
let redis_key = format!("{}:{}", RedisKeyEnum::Otp, email);
|
||||||
|
let mut conn = match self.state.redisdb.get_connection() {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(anyhow::anyhow!("Failed to get Redis connection: {}", e))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data: Option<String> = match conn.get(&redis_key) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => return Err(anyhow::anyhow!("Failed to get data from Redis: {}", e)),
|
||||||
|
};
|
||||||
|
match data {
|
||||||
|
Some(otp_str) => match otp_str.parse::<u32>() {
|
||||||
|
Ok(otp) => Ok(otp),
|
||||||
|
Err(e) => Err(anyhow::anyhow!("Failed to parse OTP as u64: {}", e)),
|
||||||
|
},
|
||||||
|
None => Err(anyhow::anyhow!("No stored OTP found")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_store_otp(&self, email: String, otp: u32) -> Result<String> {
|
||||||
|
let redis_key: String = format!("{}:{}", RedisKeyEnum::Otp, email);
|
||||||
|
let mut conn = match self.state.redisdb.get_connection() {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(e) => return Err(anyhow!("Failed to get Redis connection: {}", e)),
|
||||||
|
};
|
||||||
|
let otp_str: String = otp.to_string();
|
||||||
|
match conn.set_ex::<_, _, ()>(&redis_key, &otp_str, 300) {
|
||||||
|
Ok(_) => Ok("Success store otp".to_string()),
|
||||||
|
Err(e) => Err(anyhow!("Failed to store OTP in Redis: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_delete_stored_otp(&self, email: String) -> Result<String> {
|
||||||
|
let redis_key = format!("{}:{}", RedisKeyEnum::Otp, email);
|
||||||
|
let mut conn = match self.state.redisdb.get_connection() {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(e) => return Err(anyhow!("Failed to get Redis connection: {}", e)),
|
||||||
|
};
|
||||||
|
match conn.del::<_, ()>(&redis_key) {
|
||||||
|
Ok(_) => Ok("Successfully deleted OTP".to_string()),
|
||||||
|
Err(e) => Err(anyhow!("Failed to delete OTP from Redis: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn query_user_by_email(&self, email: String) -> Result<UsersSchema> {
|
pub async fn query_user_by_email(&self, email: String) -> Result<UsersSchema> {
|
||||||
let db = &self.state.surrealdb;
|
let db = &self.state.surrealdb;
|
||||||
let result = db.select((ResourceEnum::Users.to_string(), email)).await?;
|
let result = db
|
||||||
|
.select((ResourceEnum::Users.to_string(), email.clone()))
|
||||||
|
.await?;
|
||||||
match result {
|
match result {
|
||||||
Some(response) => Ok(response),
|
Some(response) => Ok(response),
|
||||||
None => bail!("User not found"),
|
None => {
|
||||||
|
bail!("User not found")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,13 +131,34 @@ impl<'a> AuthRepository<'a> {
|
|||||||
data: AuthActiveInactiveRequestDto,
|
data: AuthActiveInactiveRequestDto,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let db = &self.state.surrealdb;
|
let db = &self.state.surrealdb;
|
||||||
let record: Option<UsersItemDto> = db
|
let record: Option<AuthActiveInactiveRequestDto> = db
|
||||||
.update((ResourceEnum::Users.to_string(), &data.email))
|
.update((ResourceEnum::Users.to_string(), &data.email))
|
||||||
.content(data)
|
.merge(AuthActiveInactiveRequestDto {
|
||||||
|
email: data.email.clone(),
|
||||||
|
is_active: data.is_active.clone(),
|
||||||
|
})
|
||||||
.await?;
|
.await?;
|
||||||
match record {
|
match record {
|
||||||
Some(_) => Ok("Success update user".into()),
|
Some(_) => Ok("Success update user".into()),
|
||||||
None => bail!("Failed to update user"),
|
None => bail!("Failed to update user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn query_update_password_user(
|
||||||
|
&self,
|
||||||
|
data: AuthSetNewPasswordRequestDto,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb;
|
||||||
|
let record: Option<AuthSetNewPasswordRequestDto> = 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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use axum::{http::StatusCode, response::Response};
|
use axum::{http::StatusCode, response::Response};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AuthLoginRequestDto, AuthLoginResponsetDto, AuthRegisterRequestDto,
|
AuthActiveInactiveRequestDto, AuthLoginRequestDto, AuthLoginResponsetDto,
|
||||||
AuthRepository, TokenDto,
|
AuthRegisterRequestDto, AuthRepository, AuthResendOtpRequestDto,
|
||||||
|
AuthVerifyEmailRequestDto, TokenDto,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
common_response, encode_access_token, encode_refresh_token, hash_password,
|
common_response, encode_access_token, encode_refresh_token, generate_otp,
|
||||||
success_response, v1::UsersItemDto, verify_password, AppState, ResponseSuccessDto,
|
hash_password, send_email, success_response, v1::UsersItemDto, verify_password,
|
||||||
|
AppState, Env, ResponseSuccessDto,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct AuthService;
|
pub struct AuthService;
|
||||||
@@ -17,40 +19,65 @@ impl AuthService {
|
|||||||
state: &AppState,
|
state: &AppState,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let repository = AuthRepository::new(state);
|
let repository = AuthRepository::new(state);
|
||||||
|
|
||||||
match repository.query_user_by_email(payload.email.clone()).await {
|
match repository.query_user_by_email(payload.email.clone()).await {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let is_password_correct =
|
let is_password_correct =
|
||||||
verify_password(&payload.password, &user.password).unwrap_or(false);
|
verify_password(&payload.password, &user.password).unwrap_or(false);
|
||||||
|
|
||||||
if is_password_correct {
|
if !is_password_correct {
|
||||||
common_response(StatusCode::BAD_REQUEST, "Email or password not correct");
|
return common_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Email or password not correct",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let access_token = encode_access_token(payload.email.clone());
|
if !user.is_active {
|
||||||
let refresh_token = encode_refresh_token(payload.email.clone());
|
return common_response(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Account not active, please verify your email",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let access_token = match encode_access_token(payload.email.clone()) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to generate access token",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let refresh_token = match encode_refresh_token(payload.email.clone()) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to generate refresh token",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let response = ResponseSuccessDto {
|
let response = ResponseSuccessDto {
|
||||||
data: AuthLoginResponsetDto {
|
data: AuthLoginResponsetDto {
|
||||||
user: UsersItemDto {
|
user: UsersItemDto {
|
||||||
fullname: user.fullname.clone(),
|
fullname: user.fullname.clone(),
|
||||||
email: user.email.clone(),
|
email: user.email.clone(),
|
||||||
is_active: user.is_active.clone(),
|
is_active: user.is_active,
|
||||||
},
|
},
|
||||||
token: TokenDto {
|
token: TokenDto {
|
||||||
access_token: access_token.unwrap(),
|
access_token,
|
||||||
refresh_token: refresh_token.unwrap(),
|
refresh_token,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if !repository
|
if let Err(_) = repository.query_store_user_data(AuthRegisterRequestDto {
|
||||||
.query_store_user_data(AuthRegisterRequestDto {
|
fullname: user.fullname,
|
||||||
fullname: user.fullname,
|
password: user.password,
|
||||||
password: user.password,
|
email: user.email,
|
||||||
email: user.email,
|
}) {
|
||||||
})
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
return common_response(StatusCode::BAD_REQUEST, "Failed to store data");
|
return common_response(StatusCode::BAD_REQUEST, "Failed to store data");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +116,16 @@ impl AuthService {
|
|||||||
fullname: payload.fullname,
|
fullname: payload.fullname,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let otp = generate_otp::OtpManager::generate_otp();
|
||||||
|
|
||||||
|
repository
|
||||||
|
.query_store_otp(new_user.email.clone(), otp.clone())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let message = format!("your otp code is {}", otp);
|
||||||
|
|
||||||
|
send_email(&new_user.email.clone(), "OTP Verification", &message).unwrap();
|
||||||
|
|
||||||
match repository.query_create_user(new_user).await {
|
match repository.query_create_user(new_user).await {
|
||||||
Ok(_) => common_response(StatusCode::CREATED, "Registration successful"),
|
Ok(_) => common_response(StatusCode::CREATED, "Registration successful"),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -96,4 +133,98 @@ impl AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn mutation_resend_otp(
|
||||||
|
payload: AuthResendOtpRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Response {
|
||||||
|
let repository = AuthRepository::new(state);
|
||||||
|
let otp = generate_otp::OtpManager::generate_otp();
|
||||||
|
let message = format!("Your OTP code is {}", otp);
|
||||||
|
match repository.query_store_otp(payload.email.clone(), otp) {
|
||||||
|
Ok(_) => match send_email(&payload.email, "OTP Verification", &message) {
|
||||||
|
Ok(_) => common_response(StatusCode::OK, "OTP resent successfully"),
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
},
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mutation_forgot_password(
|
||||||
|
payload: AuthResendOtpRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Response {
|
||||||
|
let repository = AuthRepository::new(state);
|
||||||
|
if repository
|
||||||
|
.query_user_by_email(payload.email.clone())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||||
|
}
|
||||||
|
let token = match encode_access_token(payload.email.clone()) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to generate access token",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let env = Env::new();
|
||||||
|
let fe_url = env.fe_url;
|
||||||
|
let message = format!(
|
||||||
|
"You have requested a password reset. Please click the link below to continue: {}/auth/reset-password?token={}",
|
||||||
|
fe_url, token
|
||||||
|
);
|
||||||
|
|
||||||
|
match send_email(&payload.email, "Reset Password Request", &message) {
|
||||||
|
Ok(_) => common_response(StatusCode::OK, "Reset Password request send"),
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mutation_verify_email(
|
||||||
|
payload: AuthVerifyEmailRequestDto,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Response {
|
||||||
|
let repository = AuthRepository::new(state);
|
||||||
|
match repository.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 {
|
||||||
|
email: payload.email.clone(),
|
||||||
|
is_active: true,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
if let Err(e) =
|
||||||
|
repository.query_delete_stored_otp(payload.email.clone())
|
||||||
|
{
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&format!("Failed to delete OTP: {}", e),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
common_response(StatusCode::OK, "Email verified successfully")
|
||||||
|
}
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Err(e) = repository.query_delete_stored_otp(payload.email.clone()) {
|
||||||
|
return common_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&format!("Failed to delete OTP: {}", e),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
common_response(StatusCode::BAD_REQUEST, "Failed to verify OTP")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,7 @@ pub fn auth_router() -> Router {
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/login", post(auth_controller::post_login))
|
.route("/login", post(auth_controller::post_login))
|
||||||
.route("/register", post(auth_controller::post_register))
|
.route("/register", post(auth_controller::post_register))
|
||||||
|
.route("/verify", post(auth_controller::post_verify_email))
|
||||||
|
.route("/resend", post(auth_controller::post_resend_otp))
|
||||||
|
.route("/forgot", post(auth_controller::post_forgot_password))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
v1::{
|
v1::{
|
||||||
auth, gacha, AuthLoginRequestDto, AuthLoginResponsetDto,
|
auth, gacha, AuthLoginRequestDto, AuthLoginResponsetDto,
|
||||||
GachaCreateClaimRequestDto, GachaCreateItemRequestDto,
|
AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, GachaCreateClaimRequestDto,
|
||||||
GachaCreateRollRequestDto,
|
GachaCreateItemRequestDto, GachaCreateRollRequestDto,
|
||||||
},
|
},
|
||||||
MessageResponseDto, MetaRequestDto, MetaResponseDto, ResponseSuccessDto,
|
MessageResponseDto, MetaRequestDto, MetaResponseDto, ResponseSuccessDto,
|
||||||
};
|
};
|
||||||
@@ -17,6 +17,8 @@ use utoipa::{
|
|||||||
paths(
|
paths(
|
||||||
auth::auth_controller::post_login,
|
auth::auth_controller::post_login,
|
||||||
auth::auth_controller::post_register,
|
auth::auth_controller::post_register,
|
||||||
|
auth::auth_controller::post_verify_email,
|
||||||
|
auth::auth_controller::post_resend_otp,
|
||||||
gacha::gacha_controller::post_create_gacha_claim,
|
gacha::gacha_controller::post_create_gacha_claim,
|
||||||
gacha::gacha_controller::post_create_gacha_item,
|
gacha::gacha_controller::post_create_gacha_item,
|
||||||
gacha::gacha_controller::post_create_gacha_roll
|
gacha::gacha_controller::post_create_gacha_roll
|
||||||
@@ -28,6 +30,8 @@ use utoipa::{
|
|||||||
MessageResponseDto,
|
MessageResponseDto,
|
||||||
AuthLoginRequestDto,
|
AuthLoginRequestDto,
|
||||||
AuthLoginResponsetDto,
|
AuthLoginResponsetDto,
|
||||||
|
AuthVerifyEmailRequestDto,
|
||||||
|
AuthResendOtpRequestDto,
|
||||||
ResponseSuccessDto<AuthLoginResponsetDto>,
|
ResponseSuccessDto<AuthLoginResponsetDto>,
|
||||||
GachaCreateClaimRequestDto,
|
GachaCreateClaimRequestDto,
|
||||||
GachaCreateItemRequestDto,
|
GachaCreateItemRequestDto,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use rand::{rng, Rng};
|
||||||
|
|
||||||
|
pub struct OtpManager;
|
||||||
|
|
||||||
|
impl OtpManager {
|
||||||
|
pub fn generate_otp() -> u32 {
|
||||||
|
rng().random_range(100_000..1_000_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_otp(stored_otp: u32, user_otp: u32) -> bool {
|
||||||
|
stored_otp == user_otp
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
pub mod extract_email;
|
pub mod extract_email;
|
||||||
|
pub mod generate_otp;
|
||||||
pub mod response_format;
|
pub mod response_format;
|
||||||
|
|
||||||
pub use extract_email::*;
|
pub use extract_email::*;
|
||||||
|
pub use generate_otp::*;
|
||||||
pub use response_format::*;
|
pub use response_format::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user