diff --git a/auth/src/models/mod.rs b/auth/src/domain/mod.rs similarity index 60% rename from auth/src/models/mod.rs rename to auth/src/domain/mod.rs index 38c5726..4af8e31 100644 --- a/auth/src/models/mod.rs +++ b/auth/src/domain/mod.rs @@ -1,7 +1,3 @@ -// Migration -pub mod migration; - -// Entity pub mod permission; pub mod role; pub mod role_permissions; @@ -9,8 +5,3 @@ pub mod user_info; pub mod user_role; pub mod user_settings; pub mod users; - -pub mod common; -mod init; - -pub use init::*; diff --git a/auth/src/models/permission.rs b/auth/src/domain/permission.rs similarity index 97% rename from auth/src/models/permission.rs rename to auth/src/domain/permission.rs index 9da6aa7..6d387b8 100644 --- a/auth/src/models/permission.rs +++ b/auth/src/domain/permission.rs @@ -1,7 +1,7 @@ use sea_orm::{JoinType, QuerySelect, entity::prelude::*}; use serde::{Deserialize, Serialize}; -use crate::models::common::ModelError; +use crate::infra::database::ModelError; /// # Permission Model #[derive(Debug, Clone, PartialEq, DeriveEntityModel, Serialize, Deserialize)] diff --git a/auth/src/models/role.rs b/auth/src/domain/role.rs similarity index 97% rename from auth/src/models/role.rs rename to auth/src/domain/role.rs index 84e86b6..aa7a9a5 100644 --- a/auth/src/models/role.rs +++ b/auth/src/domain/role.rs @@ -1,7 +1,7 @@ use sea_orm::{JoinType, QuerySelect, entity::prelude::*}; use serde::{Deserialize, Serialize}; -use crate::models::common::ModelError; +use crate::infra::database::ModelError; /// # Role Model #[derive(Debug, Clone, PartialEq, DeriveEntityModel, Serialize, Deserialize)] diff --git a/auth/src/models/role_permissions.rs b/auth/src/domain/role_permissions.rs similarity index 100% rename from auth/src/models/role_permissions.rs rename to auth/src/domain/role_permissions.rs diff --git a/auth/src/models/user_info.rs b/auth/src/domain/user_info.rs similarity index 100% rename from auth/src/models/user_info.rs rename to auth/src/domain/user_info.rs diff --git a/auth/src/models/user_role.rs b/auth/src/domain/user_role.rs similarity index 100% rename from auth/src/models/user_role.rs rename to auth/src/domain/user_role.rs diff --git a/auth/src/models/user_settings.rs b/auth/src/domain/user_settings.rs similarity index 97% rename from auth/src/models/user_settings.rs rename to auth/src/domain/user_settings.rs index defb10d..00766fc 100644 --- a/auth/src/models/user_settings.rs +++ b/auth/src/domain/user_settings.rs @@ -1,7 +1,7 @@ use sea_orm::{ActiveValue::Set, entity::prelude::*}; use serde::{Deserialize, Serialize}; -use crate::models::common::ModelError::{self, DBError, Empty, ParamsError}; +use crate::infra::database::ModelError::{self, DBError, Empty, ParamsError}; /// # User Settings Model #[derive(Debug, Clone, PartialEq, DeriveEntityModel, Serialize, Deserialize)] diff --git a/auth/src/models/users.rs b/auth/src/domain/users.rs similarity index 98% rename from auth/src/models/users.rs rename to auth/src/domain/users.rs index eedbb93..13baeef 100644 --- a/auth/src/models/users.rs +++ b/auth/src/domain/users.rs @@ -2,9 +2,9 @@ use crypto::password::PasswordManager; use sea_orm::{ActiveValue::Set, IntoActiveModel, JoinType, QuerySelect, entity::prelude::*}; use serde::{Deserialize, Serialize}; -use crate::models::{ - common::ModelError::{self, DBError, Empty, ParamsError}, - permission, role, user_info, user_role, user_settings, +use crate::{ + domain::{permission, role, user_info, user_role, user_settings}, + infra::database::ModelError::{self, DBError, Empty, ParamsError}, }; /// Status of the Account diff --git a/auth/src/features/actions/mod.rs b/auth/src/features/actions/mod.rs new file mode 100644 index 0000000..563122c --- /dev/null +++ b/auth/src/features/actions/mod.rs @@ -0,0 +1,23 @@ +use axum::{Router, routing::get}; + +use crate::{ + infra::{config::Config, http::cors}, + state::AppState, +}; + +pub mod reset_password; +pub mod verify_email; + +pub fn router(config: &Config) -> Router { + let cors = if config.dev_mode { + cors::get_public_cors() + } else { + cors::get_internal_cors() + }; + + let route = Router::new() + .route("/verify-email", get(verify_email::controller)) + .route("/reset-password", get(reset_password::controller)); + + Router::new().nest("/actions", route).layer(cors) +} diff --git a/auth/src/features/actions/reset_password.rs b/auth/src/features/actions/reset_password.rs new file mode 100644 index 0000000..2ab57d9 --- /dev/null +++ b/auth/src/features/actions/reset_password.rs @@ -0,0 +1,223 @@ +use anyhow::anyhow; +use assets::AssetManager; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{Html, IntoResponse, Response}, +}; +use minijinja::{AutoEscape, Environment, context}; +use serde::Deserialize; +use token::{TokenStore, services::PasswordResetTokenService}; + +use crate::{ + infra::{ + config::Config, + error::{AppError, AppErrorKind}, + }, + state::AppState, +}; + +pub async fn controller( + State(state): State, + Query(req): Query, +) -> Response { + let token_valid = match req.token() { + Some(token) => { + let mut redis = match state.redis.get_multiplexed_tokio_connection().await { + Ok(redis) => redis, + Err(err) => { + return render_reset_password_error(AppError::infra( + AppErrorKind::InternalError, + "actions.reset_password.redis", + err, + )); + }, + }; + + match PasswordResetTokenService::get(&mut redis, token) + .await + .map(|payload| payload.is_some()) + .map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "actions.reset_password.get_token", + err, + ) + }) { + Ok(valid) => valid, + Err(err) => return render_reset_password_error(err), + } + }, + None => req.token.is_none(), + }; + + match req.render_reset_password_page(&state.config, token_valid) { + Ok(html) => Html(html).into_response(), + Err(err) => render_reset_password_error(err), + } +} + +fn render_reset_password_error(err: AppError) -> Response { + let source = err.source_ref().map(ToString::to_string); + tracing::error!( + op = err.op, + kind = ?err.kind, + detail = ?err.detail, + source = ?source, + "failed to render reset-password action page" + ); + + ( + StatusCode::INTERNAL_SERVER_ERROR, + Html( + "Reset Password \ + Error

Unable to load reset-password page. Please try again \ + later.

", + ), + ) + .into_response() +} + +const ACTION_RESET_PASSWORD_PATH: &str = "/actions/reset-password"; +const USER_INFO_API_PATH: &str = "/api/v1/user/info"; +const RESET_PASSWORD_WITH_TOKEN_API_PATH: &str = "/api/v1/auth/reset-password/token"; +const RESET_PASSWORD_WITH_PASSWORD_API_PATH: &str = "/api/v1/auth/reset-password/password"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ResetPasswordPageMode { + Token, + SessionCheck, + Invalid, +} + +#[derive(Deserialize)] +pub struct ActionsResetPasswordService { + #[serde(default)] + pub token: Option, +} + +impl ActionsResetPasswordService { + pub fn build_reset_password_action_url(config: &Config, token: &str) -> String { + format!( + "{}{}?token={token}", + config.domain.trim_end_matches('/'), + ACTION_RESET_PASSWORD_PATH + ) + } + + pub fn token(&self) -> Option<&str> { + self.token + .as_deref() + .map(str::trim) + .filter(|token| !token.is_empty()) + } + + pub fn render_reset_password_page( + &self, + config: &Config, + token_valid: bool, + ) -> Result { + let mode = self.page_mode(token_valid); + let initial_error = if mode == ResetPasswordPageMode::Invalid { + String::from("Invalid or expired reset link.") + } else { + String::new() + }; + let auth_check_api = self.auth_check_api_path(token_valid); + let submit_api = self.submit_api_path(token_valid); + let success_message = match mode { + ResetPasswordPageMode::Token => { + String::from("Password reset successfully. You can sign in with the new password.") + }, + ResetPasswordPageMode::SessionCheck => { + String::from("Password updated successfully. Please sign in again.") + }, + ResetPasswordPageMode::Invalid => String::new(), + }; + + let file = AssetManager::get("templates/actions/reset_password.html").ok_or_else(|| { + AppError::infra( + AppErrorKind::InternalError, + "actions.reset_password.load_template", + anyhow!("templates/actions/reset_password.html not found"), + ) + })?; + let source = String::from_utf8(file.data.into_owned()).map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "actions.reset_password.read_template", + err, + ) + })?; + + let mut env = Environment::new(); + env.set_auto_escape_callback(|_| AutoEscape::Html); + env.add_template("actions.reset-password", &source) + .map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "actions.reset_password.parse_template", + err, + ) + })?; + + env.get_template("actions.reset-password") + .map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "actions.reset_password.get_template", + err, + ) + })? + .render(context! { + token => self.token(), + mode => mode.as_str(), + auth_check_api => auth_check_api, + submit_api => submit_api, + site_name => config.site.name.clone(), + initial_error => initial_error, + success_message => success_message, + }) + .map_err(|err| { + AppError::infra( + AppErrorKind::InternalError, + "actions.reset_password.render_template", + err, + ) + }) + } + + fn page_mode(&self, token_valid: bool) -> ResetPasswordPageMode { + match self.token() { + Some(_) if token_valid => ResetPasswordPageMode::Token, + Some(_) => ResetPasswordPageMode::Invalid, + None if self.token.is_some() => ResetPasswordPageMode::Invalid, + None => ResetPasswordPageMode::SessionCheck, + } + } + + fn auth_check_api_path(&self, token_valid: bool) -> &'static str { + match self.page_mode(token_valid) { + ResetPasswordPageMode::SessionCheck => USER_INFO_API_PATH, + _ => "", + } + } + + fn submit_api_path(&self, token_valid: bool) -> &'static str { + match self.page_mode(token_valid) { + ResetPasswordPageMode::Token => RESET_PASSWORD_WITH_TOKEN_API_PATH, + ResetPasswordPageMode::SessionCheck => RESET_PASSWORD_WITH_PASSWORD_API_PATH, + ResetPasswordPageMode::Invalid => "", + } + } +} + +impl ResetPasswordPageMode { + fn as_str(self) -> &'static str { + match self { + ResetPasswordPageMode::Token => "token", + ResetPasswordPageMode::SessionCheck => "session-check", + ResetPasswordPageMode::Invalid => "invalid", + } + } +} diff --git a/auth/src/services/actions/verify_email.rs b/auth/src/features/actions/verify_email.rs similarity index 67% rename from auth/src/services/actions/verify_email.rs rename to auth/src/features/actions/verify_email.rs index 75a6d2f..40ac835 100644 --- a/auth/src/services/actions/verify_email.rs +++ b/auth/src/features/actions/verify_email.rs @@ -1,13 +1,49 @@ use anyhow::anyhow; use assets::AssetManager; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{Html, IntoResponse, Response}, +}; use minijinja::{AutoEscape, Environment, context}; use serde::Deserialize; -use crate::internal::{ - config::Config, - error::{AppError, AppErrorKind}, +use crate::{ + infra::{ + config::Config, + error::{AppError, AppErrorKind}, + }, + state::AppState, }; +pub async fn controller( + State(state): State, + Query(req): Query, +) -> Response { + match req.render_verify_email_page(&state.config) { + Ok(html) => Html(html).into_response(), + Err(err) => { + let source = err.source_ref().map(ToString::to_string); + tracing::error!( + op = err.op, + kind = ?err.kind, + detail = ?err.detail, + source = ?source, + "failed to render verify-email action page" + ); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Html( + "Verification \ + Error

Unable to load verification page. Please try \ + again later.

", + ), + ) + .into_response() + }, + } +} + #[derive(Deserialize)] pub struct ActionsVerifyEmailService { #[serde(default)] diff --git a/auth/src/features/api/mod.rs b/auth/src/features/api/mod.rs new file mode 100644 index 0000000..fa84959 --- /dev/null +++ b/auth/src/features/api/mod.rs @@ -0,0 +1,10 @@ +use axum::Router; + +use crate::{infra::config::Config, state::AppState}; + +pub mod v1; + +pub fn router(config: &Config) -> Router { + let route = Router::new().merge(v1::router(config)); + Router::new().nest("/api", route) +} diff --git a/auth/src/services/auth/forget_password.rs b/auth/src/features/api/v1/auth/forget_password.rs similarity index 65% rename from auth/src/services/auth/forget_password.rs rename to auth/src/features/api/v1/auth/forget_password.rs index 7681950..0650102 100644 --- a/auth/src/services/auth/forget_password.rs +++ b/auth/src/features/api/v1/auth/forget_password.rs @@ -1,3 +1,4 @@ +use axum::extract::State; use minijinja::context; use redis::aio::MultiplexedConnection; use sea_orm::DatabaseConnection; @@ -5,14 +6,52 @@ use serde::Deserialize; use token::services::PasswordResetTokenService; use crate::{ - internal::{ + domain::users, + features::actions::reset_password::ActionsResetPasswordService, + infra::{ config::Config, error::{AppError, AppErrorKind}, - mail::Mailer, + http::{ + extractor::Json, + response::app_error_to_response, + serializer::{Response, ResponseCode}, + }, + mailer::Mailer, }, - models::users, + state::AppState, }; +pub async fn controller( + State(state): State, + Json(req): Json, +) -> Response { + let mut redis = match state.redis.get_multiplexed_tokio_connection().await { + Ok(redis) => redis, + Err(err) => { + return app_error_to_response( + AppError::infra( + AppErrorKind::InternalError, + "auth.controller.forget_password.redis", + err, + ) + .with_detail("Unable to connect to redis"), + ); + }, + }; + + match req + .forget_password(&state.db, &mut redis, &state.config, state.mail.as_deref()) + .await + { + Ok(message) => Response::new( + ResponseCode::OK.into(), + ResponseCode::OK.into(), + Some(message), + ), + Err(err) => app_error_to_response(err), + } +} + const RESET_TOKEN_TTL_SECONDS: u64 = 15 * 60; #[derive(Deserialize)] @@ -60,11 +99,8 @@ impl ForgetPasswordService { ) })?; - let reset_url = format!( - "{}/api/v1/auth/reset-password?token={}", - config.domain.trim_end_matches('/'), - token - ); + let reset_url = + ActionsResetPasswordService::build_reset_password_action_url(config, &token); if let Err(err) = mailer .send_mail( diff --git a/auth/src/services/auth/login.rs b/auth/src/features/api/v1/auth/login.rs similarity index 66% rename from auth/src/services/auth/login.rs rename to auth/src/features/api/v1/auth/login.rs index f7ea337..1d9176f 100644 --- a/auth/src/services/auth/login.rs +++ b/auth/src/features/api/v1/auth/login.rs @@ -1,16 +1,62 @@ +use axum::extract::State; +use axum_extra::extract::CookieJar; use redis::aio::MultiplexedConnection; use sea_orm::DatabaseConnection; use serde::{Deserialize, Serialize}; use token::services::SessionService; use crate::{ - internal::{ + domain::users, + infra::{ error::{AppError, AppErrorKind}, + http::{ + extractor::{GuestAccess, Json}, + response::app_error_to_response, + serializer::{Response, ResponseCode}, + utils::cookie, + }, session::SESSION_TTL_SECONDS, }, - models::users, + state::AppState, }; +pub async fn controller( + _: GuestAccess, + State(state): State, + jar: CookieJar, + Json(req): Json, +) -> (CookieJar, Response) { + let mut redis = match state.redis.get_multiplexed_tokio_connection().await { + Ok(redis) => redis, + Err(err) => { + return ( + jar, + app_error_to_response( + AppError::infra( + AppErrorKind::InternalError, + "auth.controller.login.redis", + err, + ) + .with_detail("Unable to connect to redis"), + ), + ); + }, + }; + + match req.login(&state.db, &mut redis).await { + Ok((data, sid)) => { + let session_cookie = cookie::build_session_cookie(sid, !state.config.dev_mode); + let jar = jar.add(session_cookie); + + ( + jar, + Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), Some(data)), + ) + }, + Err(err) => (jar, app_error_to_response(err)), + } +} + /// Response structure for login API #[derive(Deserialize, Serialize)] pub struct LoginResponse { diff --git a/auth/src/features/api/v1/auth/logout.rs b/auth/src/features/api/v1/auth/logout.rs new file mode 100644 index 0000000..919d920 --- /dev/null +++ b/auth/src/features/api/v1/auth/logout.rs @@ -0,0 +1,59 @@ +use axum::extract::State; +use axum_extra::extract::CookieJar; +use token::services::SessionService; + +use crate::{ + infra::{ + error::{AppError, AppErrorKind}, + http::{ + extractor::LoginAccess, + response::app_error_to_response, + serializer::{Response, ResponseCode}, + utils::cookie::CookieJarExt, + }, + }, + state::AppState, +}; + +pub async fn controller( + login: LoginAccess, + State(state): State, + jar: CookieJar, +) -> (CookieJar, Response) { + let mut redis = match state.redis.get_multiplexed_tokio_connection().await { + Ok(redis) => redis, + Err(err) => { + return ( + jar, + app_error_to_response( + AppError::infra( + AppErrorKind::InternalError, + "auth.controller.logout.redis", + err, + ) + .with_detail("Unable to connect to redis"), + ), + ); + }, + }; + + let session = login.session; + + if let Err(err) = SessionService::delete(&mut redis, &session).await { + return ( + jar, + app_error_to_response(AppError::infra( + AppErrorKind::InternalError, + "auth.controller.logout.delete_session", + err, + )), + ); + } + + let jar = jar.remove_session_cookie(); + + ( + jar, + Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), None), + ) +} diff --git a/auth/src/features/api/v1/auth/mod.rs b/auth/src/features/api/v1/auth/mod.rs new file mode 100644 index 0000000..ffb8615 --- /dev/null +++ b/auth/src/features/api/v1/auth/mod.rs @@ -0,0 +1,46 @@ +use axum::{ + Router, + routing::{any, patch, post}, +}; + +use crate::{ + infra::{ + config::Config, + http::{cors, middleware::permission::PermissionAccess}, + }, + state::AppState, +}; + +mod forget_password; +mod login; +mod logout; +mod register; +mod reset_password; +mod verify_email; + +pub fn router(config: &Config) -> Router { + let cors = if config.dev_mode { + cors::get_public_cors() + } else { + cors::get_internal_cors() + }; + + let route = Router::new() + .route("/login", post(login::controller)) + .route("/logout", any(logout::controller)) + .route("/register", post(register::controller)) + .route("/verify-email", post(verify_email::controller)) + .route("/forget-password", post(forget_password::controller)) + .route( + "/reset-password/token", + patch(reset_password::controller_with_token), + ) + .route( + "/reset-password/password", + patch(reset_password::controller_with_password).layer(PermissionAccess::any(&[ + "user:reset_password:self", + "user:reset_password:other", + ])), + ); + Router::new().nest("/auth", route).layer(cors) +} diff --git a/auth/src/services/auth/register.rs b/auth/src/features/api/v1/auth/register.rs similarity index 85% rename from auth/src/services/auth/register.rs rename to auth/src/features/api/v1/auth/register.rs index ed127f8..b861299 100644 --- a/auth/src/services/auth/register.rs +++ b/auth/src/features/api/v1/auth/register.rs @@ -1,5 +1,6 @@ use std::sync::OnceLock; +use axum::extract::State; use crypto::password::{PasswordHashAlgorithm, PasswordManager}; use minijinja::context; use redis::aio::MultiplexedConnection; @@ -10,14 +11,62 @@ use token::services::RegisterTokenService; use validator::Validatable; use crate::{ - internal::{ + domain::users, + infra::{ config::Config, + database::ModelError, error::{AppError, AppErrorKind}, - mail::Mailer, + http::{ + extractor::{GuestAccess, Json}, + response::app_error_to_response, + serializer::{Response, ResponseCode}, + }, + mailer::Mailer, }, - models::{common::ModelError, users}, + state::AppState, }; +pub async fn controller( + _guest: GuestAccess, + State(state): State, + Json(req): Json, +) -> Response { + let mut redis: Option = if state.mail.is_some() { + match state.redis.get_multiplexed_tokio_connection().await { + Ok(redis) => Some(redis), + Err(err) => { + return app_error_to_response( + AppError::infra( + AppErrorKind::InternalError, + "auth.controller.register.redis", + err, + ) + .with_detail("Unable to connect to redis"), + ); + }, + } + } else { + None + }; + + match req + .register( + &state.db, + &state.config, + state.mail.as_deref(), + redis.as_mut(), + ) + .await + { + Ok(message) => Response::new( + ResponseCode::OK.into(), + ResponseCode::OK.into(), + Some(message), + ), + Err(err) => app_error_to_response(err), + } +} + static EMAIL_RE: OnceLock = OnceLock::new(); const REGISTER_TOKEN_TTL_SECONDS: u64 = 60 * 60; const REGISTER_TOKEN_REUSE_MIN_TTL_SECONDS: u64 = 60; diff --git a/auth/src/services/auth/reset_password.rs b/auth/src/features/api/v1/auth/reset_password.rs similarity index 61% rename from auth/src/services/auth/reset_password.rs rename to auth/src/features/api/v1/auth/reset_password.rs index ed06d1c..9ecdb6d 100644 --- a/auth/src/services/auth/reset_password.rs +++ b/auth/src/features/api/v1/auth/reset_password.rs @@ -1,17 +1,90 @@ +use axum::extract::State; +use axum_extra::extract::CookieJar; use crypto::password::PasswordManager; use redis::aio::MultiplexedConnection; use sea_orm::DatabaseConnection; use serde::Deserialize; -use token::services::PasswordResetTokenService; +use token::services::{PasswordResetTokenService, SessionService}; use crate::{ - internal::error::{AppError, AppErrorKind}, - models::users, + domain::users, + infra::{ + error::{AppError, AppErrorKind}, + http::{ + extractor::{Json, LoginAccess}, + response::app_error_to_response, + serializer::{Response, ResponseCode}, + utils::cookie::CookieJarExt, + }, + }, + state::AppState, }; -#[derive(Deserialize)] -pub struct ResetPasswordQuery { - pub token: String, +pub async fn controller_with_token( + State(state): State, + Json(req): Json, +) -> Response { + let mut redis = match state.redis.get_multiplexed_tokio_connection().await { + Ok(redis) => redis, + Err(err) => { + return app_error_to_response( + AppError::infra( + AppErrorKind::InternalError, + "auth.controller.reset_password_token.redis", + err, + ) + .with_detail("Unable to connect to redis"), + ); + }, + }; + + match req.reset_password(&state.db, &mut redis).await { + Ok(()) => ResponseCode::OK.into(), + Err(err) => app_error_to_response(err), + } +} + +pub async fn controller_with_password( + login: LoginAccess, + State(state): State, + jar: CookieJar, + Json(req): Json, +) -> (CookieJar, Response) { + if let Err(err) = req.reset_password(&state.db, &login.user.0).await { + return (jar, app_error_to_response(err)); + } + + let mut redis = match state.redis.get_multiplexed_tokio_connection().await { + Ok(redis) => redis, + Err(err) => { + return ( + jar, + app_error_to_response( + AppError::infra( + AppErrorKind::InternalError, + "auth.controller.reset_password_password.redis", + err, + ) + .with_detail("Unable to connect to redis"), + ), + ); + }, + }; + + if let Err(err) = SessionService::delete(&mut redis, &login.session).await { + return ( + jar, + app_error_to_response(AppError::infra( + AppErrorKind::InternalError, + "auth.controller.reset_password_password.delete_session", + err, + )), + ); + } + + let jar = jar.remove_session_cookie(); + + (jar, ResponseCode::OK.into()) } #[derive(Deserialize)] diff --git a/auth/src/services/auth/verify_email.rs b/auth/src/features/api/v1/auth/verify_email.rs similarity index 66% rename from auth/src/services/auth/verify_email.rs rename to auth/src/features/api/v1/auth/verify_email.rs index f849fda..5bb4e62 100644 --- a/auth/src/services/auth/verify_email.rs +++ b/auth/src/features/api/v1/auth/verify_email.rs @@ -1,13 +1,46 @@ +use axum::extract::State; use redis::aio::MultiplexedConnection; use sea_orm::DatabaseConnection; use serde::Deserialize; use token::{TokenStore, services::RegisterTokenService}; use crate::{ - internal::error::{AppError, AppErrorKind}, - models::users, + domain::users, + infra::{ + error::{AppError, AppErrorKind}, + http::{ + extractor::Json, + response::app_error_to_response, + serializer::{Response, ResponseCode}, + }, + }, + state::AppState, }; +pub async fn controller( + State(state): State, + Json(req): Json, +) -> Response { + let mut redis = match state.redis.get_multiplexed_tokio_connection().await { + Ok(redis) => redis, + Err(err) => { + return app_error_to_response( + AppError::infra( + AppErrorKind::InternalError, + "auth.controller.verify_email.redis", + err, + ) + .with_detail("Unable to connect to redis"), + ); + }, + }; + + match req.verify_email(&state.db, &mut redis).await { + Ok(_) => Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), None), + Err(err) => app_error_to_response(err), + } +} + #[derive(Deserialize)] pub struct VerifyEmailService { pub token: String, diff --git a/auth/src/features/api/v1/common/mod.rs b/auth/src/features/api/v1/common/mod.rs new file mode 100644 index 0000000..ad94304 --- /dev/null +++ b/auth/src/features/api/v1/common/mod.rs @@ -0,0 +1,13 @@ +use axum::{Router, routing::any}; + +use crate::{infra::http::cors, state::AppState}; + +pub mod not_found; +pub mod ping; + +pub fn router() -> Router { + Router::new() + .route("/ping", any(ping::controller)) + .fallback(not_found::controller) + .layer(cors::get_public_cors()) +} diff --git a/auth/src/features/api/v1/common/not_found.rs b/auth/src/features/api/v1/common/not_found.rs new file mode 100644 index 0000000..0eb91cb --- /dev/null +++ b/auth/src/features/api/v1/common/not_found.rs @@ -0,0 +1,5 @@ +use crate::infra::http::serializer::{Response, ResponseCode}; + +pub async fn controller() -> Response { + ResponseCode::NotFound.into() +} diff --git a/auth/src/features/api/v1/common/ping.rs b/auth/src/features/api/v1/common/ping.rs new file mode 100644 index 0000000..2cbd26a --- /dev/null +++ b/auth/src/features/api/v1/common/ping.rs @@ -0,0 +1,5 @@ +use crate::infra::http::serializer::{Response, ResponseCode}; + +pub async fn controller() -> Response { + Response::new(ResponseCode::OK.into(), "pong".into(), None) +} diff --git a/auth/src/features/api/v1/mod.rs b/auth/src/features/api/v1/mod.rs new file mode 100644 index 0000000..094ef0e --- /dev/null +++ b/auth/src/features/api/v1/mod.rs @@ -0,0 +1,15 @@ +use axum::Router; + +use crate::{infra::config::Config, state::AppState}; + +pub mod auth; +pub mod common; +pub mod users; + +pub fn router(config: &Config) -> Router { + let route = Router::new() + .merge(auth::router(config)) + .merge(users::router()) + .merge(common::router()); + Router::new().nest("/v1", route) +} diff --git a/auth/src/services/users/delete.rs b/auth/src/features/api/v1/users/delete.rs similarity index 57% rename from auth/src/services/users/delete.rs rename to auth/src/features/api/v1/users/delete.rs index 742dd1c..b15c275 100644 --- a/auth/src/services/users/delete.rs +++ b/auth/src/features/api/v1/users/delete.rs @@ -1,11 +1,81 @@ +use axum::extract::{Path, State}; +use axum_extra::extract::CookieJar; use sea_orm::DatabaseConnection; use serde::{Deserialize, Serialize}; +use token::services::SessionService; use crate::{ - internal::error::{AppError, AppErrorKind}, - models::{permission, role, users}, + domain::{permission, role, users}, + infra::{ + error::{AppError, AppErrorKind}, + http::{ + extractor::{Json, LoginAccess, OperatorAccess}, + response::app_error_to_response, + serializer::{Response, ResponseCode}, + utils::cookie::CookieJarExt, + }, + }, + state::AppState, }; +pub async fn controller( + login: LoginAccess, + State(state): State, + jar: CookieJar, + Json(req): Json, +) -> (CookieJar, Response) { + let mut redis = match state.redis.get_multiplexed_tokio_connection().await { + Ok(redis) => redis, + Err(err) => { + return ( + jar, + app_error_to_response( + AppError::infra( + AppErrorKind::InternalError, + "users.controller.delete.redis", + err, + ) + .with_detail("Unable to connect to redis"), + ), + ); + }, + }; + + let session = login.session; + + if let Err(err) = req.delete(&state.db, login.user.0).await { + return (jar, app_error_to_response(err)); + } + + if let Err(err) = SessionService::delete(&mut redis, &session).await { + return ( + jar, + app_error_to_response(AppError::infra( + AppErrorKind::InternalError, + "users.controller.delete.delete_session", + err, + )), + ); + } + + let jar = jar.remove_session_cookie(); + + (jar, ResponseCode::OK.into()) +} + +pub async fn controller_by_uid( + OperatorAccess(login): OperatorAccess, + State(state): State, + Path(uid): Path, +) -> Response { + let service = AdminDeleteService; + + match service.delete(&state.db, uid, login.level).await { + Ok(()) => ResponseCode::OK.into(), + Err(err) => app_error_to_response(err), + } +} + /// Service handling user delete operations #[derive(Deserialize, Serialize)] pub struct DeleteService { diff --git a/auth/src/services/users/info.rs b/auth/src/features/api/v1/users/info.rs similarity index 70% rename from auth/src/services/users/info.rs rename to auth/src/features/api/v1/users/info.rs index fc926ea..f0d7e00 100644 --- a/auth/src/services/users/info.rs +++ b/auth/src/features/api/v1/users/info.rs @@ -1,14 +1,49 @@ +use axum::extract::{Path, State}; use sea_orm::DatabaseConnection; use serde::{Deserialize, Serialize}; use crate::{ - internal::error::{AppError, AppErrorKind}, - models::{ + domain::{ user_info::{self, Gender}, user_settings, users, }, + infra::{ + error::{AppError, AppErrorKind}, + http::{ + extractor::{LoginAccess, OperatorAccess}, + response::app_error_to_response, + serializer::{Response, ResponseCode}, + }, + }, + state::AppState, }; +pub async fn controller( + State(state): State, + login: LoginAccess, +) -> Response { + let service = InfoService; + let (user, info, settings) = login.user; + + match service.info(&state.db, user, info, settings, None).await { + Ok(data) => Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), Some(data)), + Err(err) => app_error_to_response(err), + } +} + +pub async fn controller_by_uid( + OperatorAccess(login): OperatorAccess, + State(state): State, + Path(uid): Path, +) -> Response { + let service = InfoService; + + match service.info_by_uid(&state.db, uid, login.user.0).await { + Ok(data) => Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), Some(data)), + Err(err) => app_error_to_response(err), + } +} + #[derive(Serialize, Deserialize)] pub struct InfoResponse { pub uid: i32, diff --git a/auth/src/features/api/v1/users/mod.rs b/auth/src/features/api/v1/users/mod.rs new file mode 100644 index 0000000..3861f5c --- /dev/null +++ b/auth/src/features/api/v1/users/mod.rs @@ -0,0 +1,51 @@ +use axum::{ + Router, + routing::{any, delete, patch}, +}; + +use crate::{infra::http::middleware::permission::PermissionAccess, state::AppState}; + +mod delete; +mod info; +mod setting; +mod update; + +pub fn router() -> Router { + let route = Router::new() + .route( + "/info", + any(info::controller).layer(PermissionAccess::all(&["user:read:self"])), + ) + .route( + "/info/{uid}", + any(info::controller_by_uid).layer(PermissionAccess::any(&[ + "user:read:active", + "user:read:all", + ])), + ) + .route( + "/setting", + any(setting::controller).layer(PermissionAccess::all(&["user:read:self"])), + ) + .route( + "/setting/{uid}", + any(setting::controller_by_uid).layer(PermissionAccess::all(&["user:read:all"])), + ) + .route( + "/delete", + delete(delete::controller).layer(PermissionAccess::all(&["user:delete:self"])), + ) + .route( + "/delete/{uid}", + delete(delete::controller_by_uid).layer(PermissionAccess::all(&["user:delete:all"])), + ) + .route( + "/update", + patch(update::controller).layer(PermissionAccess::all(&["user:update:self"])), + ) + .route( + "/update/{uid}", + patch(update::controller_by_uid).layer(PermissionAccess::all(&["user:update:all"])), + ); + Router::new().nest("/user", route) +} diff --git a/auth/src/services/users/setting.rs b/auth/src/features/api/v1/users/setting.rs similarity index 64% rename from auth/src/services/users/setting.rs rename to auth/src/features/api/v1/users/setting.rs index a08d218..9a9a338 100644 --- a/auth/src/services/users/setting.rs +++ b/auth/src/features/api/v1/users/setting.rs @@ -1,11 +1,41 @@ +use axum::extract::{Path, State}; use sea_orm::DatabaseConnection; use serde::{Deserialize, Serialize}; use crate::{ - internal::error::{AppError, AppErrorKind}, - models::{common::ModelError, user_settings}, + domain::user_settings, + infra::{ + database::ModelError, + error::{AppError, AppErrorKind}, + http::{ + extractor::{LoginAccess, OperatorAccess}, + response::app_error_to_response, + serializer::{Response, ResponseCode}, + }, + }, + state::AppState, }; +pub async fn controller(login: LoginAccess) -> Response { + let service = SettingService; + let data = service.setting(login.user.2); + + Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), Some(data)) +} + +pub async fn controller_by_uid( + OperatorAccess(_login): OperatorAccess, + State(state): State, + Path(uid): Path, +) -> Response { + let service = SettingService; + + match service.setting_by_uid(&state.db, uid).await { + Ok(data) => Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), Some(data)), + Err(err) => app_error_to_response(err), + } +} + #[derive(Serialize, Deserialize)] pub struct SettingResponse { pub uid: i32, diff --git a/auth/src/services/users/update.rs b/auth/src/features/api/v1/users/update.rs similarity index 84% rename from auth/src/services/users/update.rs rename to auth/src/features/api/v1/users/update.rs index b699e24..dfa365b 100644 --- a/auth/src/services/users/update.rs +++ b/auth/src/features/api/v1/users/update.rs @@ -1,3 +1,4 @@ +use axum::extract::{Path, State}; use sea_orm::{ ActiveModelTrait, ActiveValue::Set, DatabaseConnection, DbErr, IntoActiveModel, TransactionTrait, @@ -6,14 +7,48 @@ use serde::{Deserialize, Serialize}; use validator::Validatable; use crate::{ - internal::error::{AppError, AppErrorKind}, - models::{ + domain::{ role, user_info::{self, Gender}, user_settings, users, }, + infra::{ + error::{AppError, AppErrorKind}, + http::{ + extractor::{Json, LoginAccess, OperatorAccess}, + response::app_error_to_response, + serializer::{Response, ResponseCode}, + }, + }, + state::AppState, }; +pub async fn controller( + login: LoginAccess, + State(state): State, + Json(req): Json, +) -> Response { + match req + .update(&state.db, login.user.0, login.user.1, login.user.2) + .await + { + Ok(()) => ResponseCode::OK.into(), + Err(err) => app_error_to_response(err), + } +} + +pub async fn controller_by_uid( + OperatorAccess(login): OperatorAccess, + State(state): State, + Path(uid): Path, + Json(req): Json, +) -> Response { + match req.update_by_uid(&state.db, uid, login.level).await { + Ok(()) => ResponseCode::OK.into(), + Err(err) => app_error_to_response(err), + } +} + #[derive(Deserialize, Serialize)] pub struct UpdateService { pub nickname: Option, diff --git a/auth/src/features/assets.rs b/auth/src/features/assets.rs new file mode 100644 index 0000000..24f1ef2 --- /dev/null +++ b/auth/src/features/assets.rs @@ -0,0 +1,56 @@ +use assets::AssetManager; +use axum::{ + body::Body, + extract::Request, + http::{HeaderValue, Method, Response, StatusCode, header}, + response::IntoResponse, +}; + +use crate::infra::http::utils::content_type; + +pub async fn controller(request: Request) -> impl IntoResponse { + let method = request.method().clone(); + if method != Method::GET && method != Method::HEAD { + return StatusCode::NOT_FOUND.into_response(); + } + + let raw_path = request.uri().path().trim_start_matches('/'); + let mut candidates = Vec::new(); + if raw_path.is_empty() { + candidates.push("public/index.html".to_owned()); + } else { + candidates.push(format!("public/{raw_path}")); + + if raw_path.ends_with('/') || !raw_path.contains('.') { + let trimmed = raw_path.trim_end_matches('/'); + if trimmed.is_empty() { + candidates.push("public/index.html".to_owned()); + } else { + candidates.push(format!("public/{trimmed}/index.html")); + } + } + } + + for candidate in candidates { + if let Some(file) = AssetManager::get(&candidate) { + let mut response = Response::new(Body::from(file.data.into_owned())); + *response.status_mut() = StatusCode::OK; + + if let Ok(etag) = HeaderValue::from_str(&format!( + "\"{}\"", + base16ct::lower::encode_string(&file.metadata.sha256_hash()) + )) { + response.headers_mut().insert(header::ETAG, etag); + } + + response.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static(content_type::from_path(&candidate)), + ); + + return response; + } + } + + StatusCode::NOT_FOUND.into_response() +} diff --git a/auth/src/features/mod.rs b/auth/src/features/mod.rs new file mode 100644 index 0000000..02673e9 --- /dev/null +++ b/auth/src/features/mod.rs @@ -0,0 +1,13 @@ +use axum::Router; + +use crate::{infra::config::Config, state::AppState}; + +pub mod actions; +pub mod api; +pub mod assets; + +pub fn router(app: Router, config: &Config) -> Router { + app.merge(api::router(config)) + .merge(actions::router(config)) + .fallback(assets::controller) +} diff --git a/auth/src/internal/config/common.rs b/auth/src/infra/config/common.rs similarity index 100% rename from auth/src/internal/config/common.rs rename to auth/src/infra/config/common.rs diff --git a/auth/src/internal/config/mod.rs b/auth/src/infra/config/mod.rs similarity index 100% rename from auth/src/internal/config/mod.rs rename to auth/src/infra/config/mod.rs diff --git a/auth/src/internal/config/structure/common.rs b/auth/src/infra/config/structure/common.rs similarity index 93% rename from auth/src/internal/config/structure/common.rs rename to auth/src/infra/config/structure/common.rs index 7075bd1..3baca64 100644 --- a/auth/src/internal/config/structure/common.rs +++ b/auth/src/infra/config/structure/common.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::internal::config::{Database, Mail, Redis, Secure, Site, common::CONFIG_VERSION}; +use crate::infra::config::{Database, Mail, Redis, Secure, Site, common::CONFIG_VERSION}; /// Config of madoka_auth. #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/auth/src/internal/config/structure/database.rs b/auth/src/infra/config/structure/database.rs similarity index 100% rename from auth/src/internal/config/structure/database.rs rename to auth/src/infra/config/structure/database.rs diff --git a/auth/src/internal/config/structure/mail.rs b/auth/src/infra/config/structure/mail.rs similarity index 100% rename from auth/src/internal/config/structure/mail.rs rename to auth/src/infra/config/structure/mail.rs diff --git a/auth/src/internal/config/structure/mod.rs b/auth/src/infra/config/structure/mod.rs similarity index 100% rename from auth/src/internal/config/structure/mod.rs rename to auth/src/infra/config/structure/mod.rs diff --git a/auth/src/internal/config/structure/redis.rs b/auth/src/infra/config/structure/redis.rs similarity index 100% rename from auth/src/internal/config/structure/redis.rs rename to auth/src/infra/config/structure/redis.rs diff --git a/auth/src/internal/config/structure/secure.rs b/auth/src/infra/config/structure/secure.rs similarity index 94% rename from auth/src/internal/config/structure/secure.rs rename to auth/src/infra/config/structure/secure.rs index 1271846..bc74453 100644 --- a/auth/src/internal/config/structure/secure.rs +++ b/auth/src/infra/config/structure/secure.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::internal::utils; +use crate::infra::utils; fn default_jwt_secret() -> String { utils::rand::string(16) diff --git a/auth/src/internal/config/structure/site.rs b/auth/src/infra/config/structure/site.rs similarity index 100% rename from auth/src/internal/config/structure/site.rs rename to auth/src/infra/config/structure/site.rs diff --git a/auth/src/models/common.rs b/auth/src/infra/database/error.rs similarity index 95% rename from auth/src/models/common.rs rename to auth/src/infra/database/error.rs index 0d16e07..33d60b2 100644 --- a/auth/src/models/common.rs +++ b/auth/src/infra/database/error.rs @@ -2,7 +2,7 @@ use crypto::password::PasswordError; use sea_orm::DbErr; use thiserror::Error; -use crate::internal::error::{AppError, AppErrorKind}; +use crate::infra::error::{AppError, AppErrorKind}; #[derive(Debug, Error)] pub enum ModelError { diff --git a/auth/src/models/init.rs b/auth/src/infra/database/init.rs similarity index 98% rename from auth/src/models/init.rs rename to auth/src/infra/database/init.rs index fcc773f..dad990a 100644 --- a/auth/src/models/init.rs +++ b/auth/src/infra/database/init.rs @@ -6,15 +6,17 @@ use tracing::{info, log}; use uuid::Uuid; use crate::{ - internal::{config::Database as DatabaseType, utils}, - models::{ - common::ModelError, - migration::Migrator, + domain::{ permission::{ActiveModel as PermissionActiveModel, Entity as Permission}, role::{ActiveModel as RoleActiveModel, Entity as Role}, role_permissions::{ActiveModel as RolePermissionActiveModel, Entity as RolePermission}, users::{self, AccountStatus}, }, + infra::{ + config::Database as DatabaseType, + database::{ModelError, migration::Migrator}, + utils, + }, }; pub async fn init(sql: &DatabaseType) -> Result { diff --git a/auth/src/models/migration/m20241010_000000_create_table.rs b/auth/src/infra/database/migration/m20241010_000000_create_table.rs similarity index 100% rename from auth/src/models/migration/m20241010_000000_create_table.rs rename to auth/src/infra/database/migration/m20241010_000000_create_table.rs diff --git a/auth/src/models/migration/m20241201_000000_add_userinfo.rs b/auth/src/infra/database/migration/m20241201_000000_add_userinfo.rs similarity index 100% rename from auth/src/models/migration/m20241201_000000_add_userinfo.rs rename to auth/src/infra/database/migration/m20241201_000000_add_userinfo.rs diff --git a/auth/src/models/migration/m20250709_000000_create_permission_role.rs b/auth/src/infra/database/migration/m20250709_000000_create_permission_role.rs similarity index 100% rename from auth/src/models/migration/m20250709_000000_create_permission_role.rs rename to auth/src/infra/database/migration/m20250709_000000_create_permission_role.rs diff --git a/auth/src/models/migration/m20250713_000000_create_user_role.rs b/auth/src/infra/database/migration/m20250713_000000_create_user_role.rs similarity index 100% rename from auth/src/models/migration/m20250713_000000_create_user_role.rs rename to auth/src/infra/database/migration/m20250713_000000_create_user_role.rs diff --git a/auth/src/models/migration/m20260311_000000_create_user_settings.rs b/auth/src/infra/database/migration/m20260311_000000_create_user_settings.rs similarity index 100% rename from auth/src/models/migration/m20260311_000000_create_user_settings.rs rename to auth/src/infra/database/migration/m20260311_000000_create_user_settings.rs diff --git a/auth/src/models/migration/m20260311_010000_add_audit_columns.rs b/auth/src/infra/database/migration/m20260311_010000_add_audit_columns.rs similarity index 100% rename from auth/src/models/migration/m20260311_010000_add_audit_columns.rs rename to auth/src/infra/database/migration/m20260311_010000_add_audit_columns.rs diff --git a/auth/src/models/migration/mod.rs b/auth/src/infra/database/migration/mod.rs similarity index 100% rename from auth/src/models/migration/mod.rs rename to auth/src/infra/database/migration/mod.rs diff --git a/auth/src/infra/database/mod.rs b/auth/src/infra/database/mod.rs new file mode 100644 index 0000000..bdcc8d8 --- /dev/null +++ b/auth/src/infra/database/mod.rs @@ -0,0 +1,7 @@ +pub mod migration; + +mod error; +mod init; + +pub use error::*; +pub use init::*; diff --git a/auth/src/internal/error.rs b/auth/src/infra/error.rs similarity index 100% rename from auth/src/internal/error.rs rename to auth/src/infra/error.rs diff --git a/auth/src/infra/http/cors.rs b/auth/src/infra/http/cors.rs new file mode 100644 index 0000000..637ff9e --- /dev/null +++ b/auth/src/infra/http/cors.rs @@ -0,0 +1,24 @@ +use axum::http::Method; +use tower_http::{cors, cors::CorsLayer}; + +pub fn get_public_cors() -> CorsLayer { + CorsLayer::new() + .allow_methods([ + Method::GET, + Method::POST, + Method::PATCH, + Method::DELETE, + Method::OPTIONS, + ]) + .allow_origin(cors::Any) +} + +pub fn get_internal_cors() -> CorsLayer { + CorsLayer::new().allow_methods([ + Method::GET, + Method::POST, + Method::PATCH, + Method::DELETE, + Method::OPTIONS, + ]) +} diff --git a/auth/src/routers/extractor/guest.rs b/auth/src/infra/http/extractor/guest.rs similarity index 94% rename from auth/src/routers/extractor/guest.rs rename to auth/src/infra/http/extractor/guest.rs index dc1d234..1cc9550 100644 --- a/auth/src/routers/extractor/guest.rs +++ b/auth/src/infra/http/extractor/guest.rs @@ -2,7 +2,7 @@ use axum::{extract::FromRequestParts, http::request::Parts}; use axum_extra::extract::CookieJar; use token::services::{SessionLookup, SessionService}; -use crate::{routers::serializer::ResponseCode, state::AppState}; +use crate::{infra::http::serializer::ResponseCode, state::AppState}; pub struct GuestAccess; diff --git a/auth/src/routers/extractor/json.rs b/auth/src/infra/http/extractor/json.rs similarity index 93% rename from auth/src/routers/extractor/json.rs rename to auth/src/infra/http/extractor/json.rs index 3ee14c0..b080c41 100644 --- a/auth/src/routers/extractor/json.rs +++ b/auth/src/infra/http/extractor/json.rs @@ -1,7 +1,7 @@ use axum::extract::{FromRequest, Request, rejection::JsonRejection}; use serde::de::DeserializeOwned; -use crate::routers::serializer::{Response, ResponseCode}; +use crate::infra::http::serializer::{Response, ResponseCode}; #[derive(Debug, Clone, Copy, Default)] pub struct Json(pub T); diff --git a/auth/src/routers/extractor/login.rs b/auth/src/infra/http/extractor/login.rs similarity index 95% rename from auth/src/routers/extractor/login.rs rename to auth/src/infra/http/extractor/login.rs index d28813e..502dffc 100644 --- a/auth/src/routers/extractor/login.rs +++ b/auth/src/infra/http/extractor/login.rs @@ -3,8 +3,8 @@ use axum_extra::extract::CookieJar; use token::services::{SessionLookup, SessionService}; use crate::{ - models::{role, user_info, user_settings, users}, - routers::serializer::ResponseCode, + domain::{role, user_info, user_settings, users}, + infra::http::serializer::ResponseCode, state::AppState, }; diff --git a/auth/src/routers/extractor/mod.rs b/auth/src/infra/http/extractor/mod.rs similarity index 100% rename from auth/src/routers/extractor/mod.rs rename to auth/src/infra/http/extractor/mod.rs diff --git a/auth/src/routers/extractor/operator.rs b/auth/src/infra/http/extractor/operator.rs similarity index 90% rename from auth/src/routers/extractor/operator.rs rename to auth/src/infra/http/extractor/operator.rs index 3b5b12b..8d6c26f 100644 --- a/auth/src/routers/extractor/operator.rs +++ b/auth/src/infra/http/extractor/operator.rs @@ -1,7 +1,7 @@ use axum::{extract::FromRequestParts, http::request::Parts}; use crate::{ - routers::{extractor::LoginAccess, serializer::ResponseCode}, + infra::http::{extractor::LoginAccess, serializer::ResponseCode}, state::AppState, }; diff --git a/auth/src/routers/middleware/mod.rs b/auth/src/infra/http/middleware/mod.rs similarity index 100% rename from auth/src/routers/middleware/mod.rs rename to auth/src/infra/http/middleware/mod.rs diff --git a/auth/src/routers/middleware/permission.rs b/auth/src/infra/http/middleware/permission.rs similarity index 98% rename from auth/src/routers/middleware/permission.rs rename to auth/src/infra/http/middleware/permission.rs index 6a685c2..c1799c2 100644 --- a/auth/src/routers/middleware/permission.rs +++ b/auth/src/infra/http/middleware/permission.rs @@ -9,7 +9,7 @@ use axum_extra::extract::cookie::Cookie; use token::services::{SessionLookup, SessionService}; use tower::{Layer, Service}; -use crate::{models::permission, routers::serializer::ResponseCode, state::APP_STATE}; +use crate::{domain::permission, infra::http::serializer::ResponseCode, state::APP_STATE}; #[derive(Clone)] enum PermType { diff --git a/auth/src/infra/http/mod.rs b/auth/src/infra/http/mod.rs new file mode 100644 index 0000000..663740b --- /dev/null +++ b/auth/src/infra/http/mod.rs @@ -0,0 +1,6 @@ +pub mod cors; +pub mod extractor; +pub mod middleware; +pub mod response; +pub mod serializer; +pub mod utils; diff --git a/auth/src/routers/response.rs b/auth/src/infra/http/response.rs similarity index 95% rename from auth/src/routers/response.rs rename to auth/src/infra/http/response.rs index 67b2261..76e20b1 100644 --- a/auth/src/routers/response.rs +++ b/auth/src/infra/http/response.rs @@ -1,6 +1,6 @@ -use crate::{ - internal::error::{AppError, AppErrorKind}, - routers::serializer::{Response, ResponseCode}, +use crate::infra::{ + error::{AppError, AppErrorKind}, + http::serializer::{Response, ResponseCode}, }; impl From for ResponseCode { diff --git a/auth/src/routers/serializer/common.rs b/auth/src/infra/http/serializer/common.rs similarity index 100% rename from auth/src/routers/serializer/common.rs rename to auth/src/infra/http/serializer/common.rs diff --git a/auth/src/routers/serializer/error.rs b/auth/src/infra/http/serializer/error.rs similarity index 100% rename from auth/src/routers/serializer/error.rs rename to auth/src/infra/http/serializer/error.rs diff --git a/auth/src/routers/serializer/mod.rs b/auth/src/infra/http/serializer/mod.rs similarity index 100% rename from auth/src/routers/serializer/mod.rs rename to auth/src/infra/http/serializer/mod.rs diff --git a/auth/src/routers/utils/content_type.rs b/auth/src/infra/http/utils/content_type.rs similarity index 100% rename from auth/src/routers/utils/content_type.rs rename to auth/src/infra/http/utils/content_type.rs diff --git a/auth/src/routers/utils/cookie.rs b/auth/src/infra/http/utils/cookie.rs similarity index 92% rename from auth/src/routers/utils/cookie.rs rename to auth/src/infra/http/utils/cookie.rs index f47104c..22fc71d 100644 --- a/auth/src/routers/utils/cookie.rs +++ b/auth/src/infra/http/utils/cookie.rs @@ -3,7 +3,7 @@ use axum_extra::extract::{ cookie::{Cookie, SameSite}, }; -use crate::internal::session::SESSION_TTL_SECONDS; +use crate::infra::session::SESSION_TTL_SECONDS; pub trait CookieJarExt { fn remove_session_cookie(self) -> Self; diff --git a/auth/src/routers/utils/mod.rs b/auth/src/infra/http/utils/mod.rs similarity index 100% rename from auth/src/routers/utils/mod.rs rename to auth/src/infra/http/utils/mod.rs diff --git a/auth/src/internal/log/layer.rs b/auth/src/infra/logger/layer.rs similarity index 100% rename from auth/src/internal/log/layer.rs rename to auth/src/infra/logger/layer.rs diff --git a/auth/src/internal/log/mod.rs b/auth/src/infra/logger/mod.rs similarity index 100% rename from auth/src/internal/log/mod.rs rename to auth/src/infra/logger/mod.rs diff --git a/auth/src/internal/log/visitor.rs b/auth/src/infra/logger/visitor.rs similarity index 100% rename from auth/src/internal/log/visitor.rs rename to auth/src/infra/logger/visitor.rs diff --git a/auth/src/internal/mail.rs b/auth/src/infra/mailer.rs similarity index 98% rename from auth/src/internal/mail.rs rename to auth/src/infra/mailer.rs index fd8388c..8f913fe 100644 --- a/auth/src/internal/mail.rs +++ b/auth/src/infra/mailer.rs @@ -9,7 +9,7 @@ use lettre::{ }; use minijinja::{AutoEscape, Environment, Value}; -use crate::internal::config::{Mail, MailSecure}; +use crate::infra::config::{Mail, MailSecure}; pub fn init(config: &Mail) -> Result { Mailer::new(config) diff --git a/auth/src/infra/mod.rs b/auth/src/infra/mod.rs new file mode 100644 index 0000000..e9a3b6c --- /dev/null +++ b/auth/src/infra/mod.rs @@ -0,0 +1,8 @@ +pub mod config; +pub mod database; +pub mod error; +pub mod http; +pub mod logger; +pub mod mailer; +pub mod session; +pub mod utils; diff --git a/auth/src/internal/session.rs b/auth/src/infra/session.rs similarity index 100% rename from auth/src/internal/session.rs rename to auth/src/infra/session.rs diff --git a/auth/src/internal/utils/mod.rs b/auth/src/infra/utils/mod.rs similarity index 100% rename from auth/src/internal/utils/mod.rs rename to auth/src/infra/utils/mod.rs diff --git a/auth/src/internal/utils/rand.rs b/auth/src/infra/utils/rand.rs similarity index 100% rename from auth/src/internal/utils/rand.rs rename to auth/src/infra/utils/rand.rs diff --git a/auth/src/init.rs b/auth/src/init.rs index 198978d..f1386f2 100644 --- a/auth/src/init.rs +++ b/auth/src/init.rs @@ -4,8 +4,8 @@ use tracing_subscriber::{ Layer, filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, }; -use crate::internal::{config::Redis, log::layer::LogLayer}; -pub use crate::{internal::mail::init as mail, models::init as db}; +use crate::infra::{config::Redis, logger::layer::LogLayer}; +pub use crate::infra::{database::init as db, mailer::init as mailer}; /// Initialize the logger pub fn logger() { diff --git a/auth/src/internal/mod.rs b/auth/src/internal/mod.rs deleted file mode 100644 index 22a158d..0000000 --- a/auth/src/internal/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Configuration module -pub mod config; - -// Logging module -pub mod log; - -// Error module -pub mod error; - -// Session module -pub mod session; - -// Utility module -pub mod utils; - -// Mail module -pub mod mail; diff --git a/auth/src/main.rs b/auth/src/main.rs index c23389b..25cda98 100644 --- a/auth/src/main.rs +++ b/auth/src/main.rs @@ -3,10 +3,9 @@ mod init; -mod internal; -mod models; -mod routers; -mod services; +mod domain; +mod features; +mod infra; mod state; use std::{io, sync::Arc}; @@ -16,7 +15,7 @@ use colored::Colorize; use tokio::{net::TcpListener, signal, sync::oneshot}; use tracing::{error, info}; -use crate::{internal::config::Config, routers::get_router}; +use crate::infra::config::Config; async fn shutdown_signal() { let ctrl_c = async { @@ -70,7 +69,7 @@ async fn main() -> anyhow::Result<()> { info!("Redis initialized."); let mail = if let Some(mail) = &config.mail { - Some(Arc::new(init::mail(mail)?)) + Some(Arc::new(init::mailer(mail)?)) } else { None }; @@ -87,8 +86,8 @@ async fn main() -> anyhow::Result<()> { }) .await; - let app = - get_router(Router::new(), &config).with_state(state::APP_STATE.get().unwrap().clone()); + let app = features::router(Router::new(), &config) + .with_state(state::APP_STATE.get().unwrap().clone()); let addr = format!("{}:{}", &host, config.port); diff --git a/auth/src/routers.rs b/auth/src/routers.rs deleted file mode 100644 index b50ebac..0000000 --- a/auth/src/routers.rs +++ /dev/null @@ -1,207 +0,0 @@ -pub mod controllers; -pub mod extractor; -pub mod middleware; -pub mod response; -pub mod serializer; -pub mod utils; - -use assets::AssetManager; -use axum::{ - Router, - body::Body, - extract::Request, - http::{HeaderValue, Method, StatusCode, header}, - response::{IntoResponse, Response}, - routing::{any, delete, get, patch, post}, -}; -use tower::ServiceBuilder; -use tower_http::{cors, cors::CorsLayer}; - -use crate::{ - internal::config::Config, - routers::{ - controllers::{actions, auth, common, users}, - middleware::permission::PermissionAccess, - utils::content_type, - }, - state::AppState, -}; - -pub fn get_router(app: Router, config: &Config) -> Router { - // CORS - let public_cors = { - let cors = CorsLayer::new() - .allow_methods([ - Method::GET, - Method::POST, - Method::PATCH, - Method::DELETE, - Method::OPTIONS, - ]) - .allow_origin(cors::Any); - ServiceBuilder::new().layer(cors).into_inner() - }; - let internal_cors = { - let cors = CorsLayer::new().allow_methods([ - Method::GET, - Method::POST, - Method::PATCH, - Method::DELETE, - Method::OPTIONS, - ]); - ServiceBuilder::new().layer(cors).into_inner() - }; - - // Oauth - let oauth = { - let oauth = Router::new(); - Router::new().nest("/oauth", oauth) - }; - - let action = { - let route = Router::new().route("/verify-email", get(actions::verify_email)); - let route = Router::new().nest("/actions", route); - if config.dev_mode { - route.layer(public_cors.clone()) - } else { - route.layer(internal_cors.clone()) - } - }; - - let api_v1 = { - // Auth - let auth = { - let route = Router::new() - .route("/login", post(auth::login)) - .route("/logout", any(auth::logout)) - .route("/register", post(auth::register)) - .route("/verify-email", post(auth::verify_email)) - .route("/forget-password", post(auth::forget_password)) - .route("/reset-password", get(auth::reset_password)) - .route( - "/reset-password/token", - patch(auth::reset_password_with_token), - ) - .route( - "/reset-password/password", - patch(auth::reset_password_with_password).layer(PermissionAccess::any(&[ - "user:reset_password:self", - "user:reset_password:other", - ])), - ); - let route = Router::new().nest("/auth", route); - if config.dev_mode { - route.layer(public_cors.clone()) - } else { - route.layer(internal_cors.clone()) - } - }; - - // User - let user = { - let route = Router::new() - .route( - "/info", - any(users::info).layer(PermissionAccess::all(&["user:read:self"])), - ) - .route( - "/info/{uid}", - any(users::info_by_uid).layer(PermissionAccess::any(&[ - "user:read:active", - "user:read:all", - ])), - ) - .route( - "/setting", - any(users::setting).layer(PermissionAccess::all(&["user:read:self"])), - ) - .route( - "/setting/{uid}", - any(users::setting_by_uid).layer(PermissionAccess::all(&["user:read:all"])), - ) - .route( - "/delete", - delete(users::delete).layer(PermissionAccess::all(&["user:delete:self"])), - ) - .route( - "/delete/{uid}", - delete(users::delete_by_uid).layer(PermissionAccess::all(&["user:delete:all"])), - ) - .route( - "/update", - patch(users::update).layer(PermissionAccess::all(&["user:update:self"])), - ) - .route( - "/update/{uid}", - patch(users::update_by_uid).layer(PermissionAccess::all(&["user:update:all"])), - ); - Router::new().nest("/user", route) - }; - - let common = Router::new() - .route("/ping", any(common::ping)) - .fallback(common::not_found) - .layer(public_cors); - - let route = Router::new().merge(auth).merge(user).merge(common); - Router::new().nest("/v1", route) - }; - - // API - let api = { - let route = Router::new().merge(api_v1); - Router::new().nest("/api", route) - }; - - app.merge(api) - .merge(oauth) - .merge(action) - .fallback(static_asset_fallback) -} - -async fn static_asset_fallback(request: Request) -> impl IntoResponse { - let method = request.method().clone(); - if method != Method::GET && method != Method::HEAD { - return StatusCode::NOT_FOUND.into_response(); - } - - let raw_path = request.uri().path().trim_start_matches('/'); - let mut candidates = Vec::new(); - if raw_path.is_empty() { - candidates.push("public/index.html".to_owned()); - } else { - candidates.push(format!("public/{raw_path}")); - - if raw_path.ends_with('/') || !raw_path.contains('.') { - let trimmed = raw_path.trim_end_matches('/'); - if trimmed.is_empty() { - candidates.push("public/index.html".to_owned()); - } else { - candidates.push(format!("public/{trimmed}/index.html")); - } - } - } - - for candidate in candidates { - if let Some(file) = AssetManager::get(&candidate) { - let mut response = Response::new(Body::from(file.data.into_owned())); - *response.status_mut() = StatusCode::OK; - - if let Ok(etag) = HeaderValue::from_str(&format!( - "\"{}\"", - base16ct::lower::encode_string(&file.metadata.sha256_hash()) - )) { - response.headers_mut().insert(header::ETAG, etag); - } - - response.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static(content_type::from_path(&candidate)), - ); - - return response; - } - } - - StatusCode::NOT_FOUND.into_response() -} diff --git a/auth/src/routers/controllers.rs b/auth/src/routers/controllers.rs deleted file mode 100644 index 8283a8e..0000000 --- a/auth/src/routers/controllers.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod actions; -pub mod auth; -pub mod common; -pub mod users; diff --git a/auth/src/routers/controllers/actions.rs b/auth/src/routers/controllers/actions.rs deleted file mode 100644 index ca26806..0000000 --- a/auth/src/routers/controllers/actions.rs +++ /dev/null @@ -1,35 +0,0 @@ -use axum::{ - extract::{Query, State}, - http::StatusCode, - response::{Html, IntoResponse, Response as AxumResponse}, -}; - -use crate::{services::actions::ActionsVerifyEmailService, state::AppState}; - -pub async fn verify_email( - State(state): State, - Query(req): Query, -) -> AxumResponse { - match req.render_verify_email_page(&state.config) { - Ok(html) => Html(html).into_response(), - Err(err) => { - let source = err.source_ref().map(ToString::to_string); - tracing::error!( - op = err.op, - kind = ?err.kind, - detail = ?err.detail, - source = ?source, - "failed to render verify-email action page" - ); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Html( - "Verification \ - Error

Unable to load verification page. Please try \ - again later.

", - ), - ) - .into_response() - }, - } -} diff --git a/auth/src/routers/controllers/auth.rs b/auth/src/routers/controllers/auth.rs deleted file mode 100644 index 509afa1..0000000 --- a/auth/src/routers/controllers/auth.rs +++ /dev/null @@ -1,275 +0,0 @@ -use axum::{ - extract::{Query, State}, - http::StatusCode, -}; -use axum_extra::extract::CookieJar; -use redis::aio::MultiplexedConnection; -use token::services::SessionService; - -use crate::{ - internal::error::{AppError, AppErrorKind}, - routers::{ - extractor::{GuestAccess, Json, LoginAccess}, - response::app_error_to_response, - serializer::{Response, ResponseCode}, - utils::cookie::{self, CookieJarExt}, - }, - services::auth, - state::AppState, -}; - -/// Auth register -pub async fn register( - _guest: GuestAccess, - State(state): State, - Json(req): Json, -) -> Response { - let mut redis: Option = if state.mail.is_some() { - match state.redis.get_multiplexed_tokio_connection().await { - Ok(redis) => Some(redis), - Err(err) => { - return app_error_to_response( - AppError::infra( - AppErrorKind::InternalError, - "auth.controller.register.redis", - err, - ) - .with_detail("Unable to connect to redis"), - ); - }, - } - } else { - None - }; - - match req - .register( - &state.db, - &state.config, - state.mail.as_deref(), - redis.as_mut(), - ) - .await - { - Ok(message) => Response::new( - ResponseCode::OK.into(), - ResponseCode::OK.into(), - Some(message), - ), - Err(err) => app_error_to_response(err), - } -} - -/// Auth login -pub async fn login( - _: GuestAccess, - State(state): State, - jar: CookieJar, - Json(req): Json, -) -> (CookieJar, Response) { - let mut redis = match state.redis.get_multiplexed_tokio_connection().await { - Ok(redis) => redis, - Err(err) => { - return ( - jar, - app_error_to_response( - AppError::infra( - AppErrorKind::InternalError, - "auth.controller.login.redis", - err, - ) - .with_detail("Unable to connect to redis"), - ), - ); - }, - }; - - match req.login(&state.db, &mut redis).await { - Ok((data, sid)) => { - let session_cookie = cookie::build_session_cookie(sid, !state.config.dev_mode); - let jar = jar.add(session_cookie); - - ( - jar, - Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), Some(data)), - ) - }, - Err(err) => (jar, app_error_to_response(err)), - } -} - -/// Auth logout -pub async fn logout( - login: LoginAccess, - State(state): State, - jar: CookieJar, -) -> (CookieJar, Response) { - let mut redis = match state.redis.get_multiplexed_tokio_connection().await { - Ok(redis) => redis, - Err(err) => { - return ( - jar, - app_error_to_response( - AppError::infra( - AppErrorKind::InternalError, - "auth.controller.logout.redis", - err, - ) - .with_detail("Unable to connect to redis"), - ), - ); - }, - }; - - let session = login.session; - - if let Err(err) = SessionService::delete(&mut redis, &session).await { - return ( - jar, - app_error_to_response(AppError::infra( - AppErrorKind::InternalError, - "auth.controller.logout.delete_session", - err, - )), - ); - } - - let jar = jar.remove_session_cookie(); - - ( - jar, - Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), None), - ) -} - -/// Auth reset password page placeholder -pub async fn reset_password(Query(query): Query) -> StatusCode { - // TODO: Reset Password Page - let _token = query.token; - StatusCode::OK -} - -/// Auth reset password with token -pub async fn reset_password_with_token( - State(state): State, - Json(req): Json, -) -> Response { - let mut redis = match state.redis.get_multiplexed_tokio_connection().await { - Ok(redis) => redis, - Err(err) => { - return app_error_to_response( - AppError::infra( - AppErrorKind::InternalError, - "auth.controller.reset_password_token.redis", - err, - ) - .with_detail("Unable to connect to redis"), - ); - }, - }; - - match req.reset_password(&state.db, &mut redis).await { - Ok(()) => ResponseCode::OK.into(), - Err(err) => app_error_to_response(err), - } -} - -/// Auth reset password with current password -pub async fn reset_password_with_password( - login: LoginAccess, - State(state): State, - jar: CookieJar, - Json(req): Json, -) -> (CookieJar, Response) { - if let Err(err) = req.reset_password(&state.db, &login.user.0).await { - return (jar, app_error_to_response(err)); - } - - let mut redis = match state.redis.get_multiplexed_tokio_connection().await { - Ok(redis) => redis, - Err(err) => { - return ( - jar, - app_error_to_response( - AppError::infra( - AppErrorKind::InternalError, - "auth.controller.reset_password_password.redis", - err, - ) - .with_detail("Unable to connect to redis"), - ), - ); - }, - }; - - if let Err(err) = SessionService::delete(&mut redis, &login.session).await { - return ( - jar, - app_error_to_response(AppError::infra( - AppErrorKind::InternalError, - "auth.controller.reset_password_password.delete_session", - err, - )), - ); - } - - let jar = jar.remove_session_cookie(); - - (jar, ResponseCode::OK.into()) -} - -/// Auth forget password -pub async fn forget_password( - State(state): State, - Json(req): Json, -) -> Response { - let mut redis = match state.redis.get_multiplexed_tokio_connection().await { - Ok(redis) => redis, - Err(err) => { - return app_error_to_response( - AppError::infra( - AppErrorKind::InternalError, - "auth.controller.forget_password.redis", - err, - ) - .with_detail("Unable to connect to redis"), - ); - }, - }; - - match req - .forget_password(&state.db, &mut redis, &state.config, state.mail.as_deref()) - .await - { - Ok(message) => Response::new( - ResponseCode::OK.into(), - ResponseCode::OK.into(), - Some(message), - ), - Err(err) => app_error_to_response(err), - } -} - -pub async fn verify_email( - State(state): State, - Json(req): Json, -) -> Response { - let mut redis = match state.redis.get_multiplexed_tokio_connection().await { - Ok(redis) => redis, - Err(err) => { - return app_error_to_response( - AppError::infra( - AppErrorKind::InternalError, - "auth.controller.verify_email.redis", - err, - ) - .with_detail("Unable to connect to redis"), - ); - }, - }; - - match req.verify_email(&state.db, &mut redis).await { - Ok(_) => Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), None), - Err(err) => app_error_to_response(err), - } -} diff --git a/auth/src/routers/controllers/common.rs b/auth/src/routers/controllers/common.rs deleted file mode 100644 index e5fdb72..0000000 --- a/auth/src/routers/controllers/common.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::routers::serializer::{Response, ResponseCode}; - -pub async fn not_found() -> Response { - ResponseCode::NotFound.into() -} - -pub async fn ping() -> Response { - Response::new(ResponseCode::OK.into(), "pong".into(), None) -} diff --git a/auth/src/routers/controllers/users.rs b/auth/src/routers/controllers/users.rs deleted file mode 100644 index bba0b56..0000000 --- a/auth/src/routers/controllers/users.rs +++ /dev/null @@ -1,153 +0,0 @@ -use axum::extract::{Path, State}; -use axum_extra::extract::CookieJar; -use token::services::SessionService; - -use crate::{ - internal::error::{AppError, AppErrorKind}, - routers::{ - extractor::{Json, LoginAccess, OperatorAccess}, - response::app_error_to_response, - serializer::{Response, ResponseCode}, - utils::cookie::CookieJarExt, - }, - services::users, - state::AppState, -}; - -/// User info -pub async fn info( - State(state): State, - login: LoginAccess, -) -> Response { - let service = users::InfoService; - let (user, info, settings) = login.user; - - match service.info(&state.db, user, info, settings, None).await { - Ok(data) => Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), Some(data)), - Err(err) => app_error_to_response(err), - } -} - -/// User setting -pub async fn setting(login: LoginAccess) -> Response { - let service = users::SettingService; - let data = service.setting(login.user.2); - - Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), Some(data)) -} - -/// User setting by uid -pub async fn setting_by_uid( - OperatorAccess(_login): OperatorAccess, - State(state): State, - Path(uid): Path, -) -> Response { - let service = users::SettingService; - - match service.setting_by_uid(&state.db, uid).await { - Ok(data) => Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), Some(data)), - Err(err) => app_error_to_response(err), - } -} - -/// User info by uid -pub async fn info_by_uid( - OperatorAccess(login): OperatorAccess, - State(state): State, - Path(uid): Path, -) -> Response { - let service = users::InfoService; - - match service.info_by_uid(&state.db, uid, login.user.0).await { - Ok(data) => Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), Some(data)), - Err(err) => app_error_to_response(err), - } -} - -/// User delete -pub async fn delete( - login: LoginAccess, - State(state): State, - jar: CookieJar, - Json(req): Json, -) -> (CookieJar, Response) { - let mut redis = match state.redis.get_multiplexed_tokio_connection().await { - Ok(redis) => redis, - Err(err) => { - return ( - jar, - app_error_to_response( - AppError::infra( - AppErrorKind::InternalError, - "users.controller.delete.redis", - err, - ) - .with_detail("Unable to connect to redis"), - ), - ); - }, - }; - - let session = login.session; - - if let Err(err) = req.delete(&state.db, login.user.0).await { - return (jar, app_error_to_response(err)); - } - - if let Err(err) = SessionService::delete(&mut redis, &session).await { - return ( - jar, - app_error_to_response(AppError::infra( - AppErrorKind::InternalError, - "users.controller.delete.delete_session", - err, - )), - ); - } - - let jar = jar.remove_session_cookie(); - - (jar, ResponseCode::OK.into()) -} - -/// User delete by uid -pub async fn delete_by_uid( - OperatorAccess(login): OperatorAccess, - State(state): State, - Path(uid): Path, -) -> Response { - let service = users::AdminDeleteService; - - match service.delete(&state.db, uid, login.level).await { - Ok(()) => ResponseCode::OK.into(), - Err(err) => app_error_to_response(err), - } -} - -/// User update -pub async fn update( - login: LoginAccess, - State(state): State, - Json(req): Json, -) -> Response { - match req - .update(&state.db, login.user.0, login.user.1, login.user.2) - .await - { - Ok(()) => ResponseCode::OK.into(), - Err(err) => app_error_to_response(err), - } -} - -/// User update by uid -pub async fn update_by_uid( - OperatorAccess(login): OperatorAccess, - State(state): State, - Path(uid): Path, - Json(req): Json, -) -> Response { - match req.update_by_uid(&state.db, uid, login.level).await { - Ok(()) => ResponseCode::OK.into(), - Err(err) => app_error_to_response(err), - } -} diff --git a/auth/src/services/actions/mod.rs b/auth/src/services/actions/mod.rs deleted file mode 100644 index 7b3c454..0000000 --- a/auth/src/services/actions/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod verify_email; - -pub use verify_email::*; diff --git a/auth/src/services/auth/mod.rs b/auth/src/services/auth/mod.rs deleted file mode 100644 index 3c3cf1b..0000000 --- a/auth/src/services/auth/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod forget_password; -mod login; -mod register; -mod reset_password; -mod verify_email; - -pub use forget_password::*; -pub use login::*; -pub use register::*; -pub use reset_password::*; -pub use verify_email::*; diff --git a/auth/src/services/mod.rs b/auth/src/services/mod.rs deleted file mode 100644 index 189c4f5..0000000 --- a/auth/src/services/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod actions; -pub mod auth; -pub mod users; diff --git a/auth/src/services/users/mod.rs b/auth/src/services/users/mod.rs deleted file mode 100644 index 88448d5..0000000 --- a/auth/src/services/users/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod delete; -mod info; -mod setting; -mod update; - -pub use delete::*; -pub use info::*; -pub use setting::*; -pub use update::*; diff --git a/auth/src/state.rs b/auth/src/state.rs index d13605b..3ac86f5 100644 --- a/auth/src/state.rs +++ b/auth/src/state.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use sea_orm::DatabaseConnection; use tokio::sync::OnceCell; -use crate::internal::{config, mail::Mailer}; +use crate::infra::{config, mailer::Mailer}; pub static APP_STATE: OnceCell = OnceCell::const_new(); diff --git a/crates/assets/assets/templates/actions/reset_password.html b/crates/assets/assets/templates/actions/reset_password.html new file mode 100644 index 0000000..ac7c186 --- /dev/null +++ b/crates/assets/assets/templates/actions/reset_password.html @@ -0,0 +1,399 @@ + + + + + + Reset Password - {{ site_name }} + + + +
+

Account Security

+

Reset your password

+

+ +
+ + + + + + + + +

+
+ + + +