Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
database_url=postgresql://username:password@host:port/db_name
redis_url=redis://127.0.0.1:6379
sentry_dsn=
smtp_host=smtp.example.com
smtp_port=587
smtp_username=your_smtp_username
smtp_password=your_smtp_password
smtp_from=no-reply@example.com
61 changes: 61 additions & 0 deletions apps/backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ rand = "0.10"
subtle = "2.4"
strum = { version = "0.28.0", features = ["derive"] }
strum_macros = "0.28"
lettre = { version = "0.11", default-features = false, features = ["builder", "hostname", "smtp-transport","tokio1", "tokio1-rustls-tls"] }
6 changes: 5 additions & 1 deletion apps/backend/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::{settings::Settings, utils::redis::RedisConnection};
use crate::{
settings::Settings,
utils::{redis::RedisConnection, smtp::SmtpClient},
};
use sea_orm::DatabaseConnection;

pub mod dto;
Expand All @@ -17,4 +20,5 @@ pub struct AppState {
pub settings: Settings,
pub db: DatabaseConnection,
pub redis_client: RedisConnection,
pub smtp_client: SmtpClient,
}
14 changes: 13 additions & 1 deletion apps/backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use backend::{AppState, server::run};
use backend::{AppState, server::run, utils::smtp::SmtpClient};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
Expand All @@ -19,12 +19,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.sync(&db)
.await?;

let smtp_client = SmtpClient::new(
&settings.smtp_host,
settings.smtp_port,
&settings.smtp_username,
&settings.smtp_password,
)
.map_err(|err| {
std::io::Error::other(format!(
"SMTP client initialization failed. If email is required in this environment, check smtp_host/smtp_port/smtp_username/smtp_password. Underlying error: {err}"
))
})?;
let redis_client = backend::utils::redis::RedisConnection::new(&settings.redis_url);
redis_client.ping().await?;
let state = AppState {
settings,
db,
redis_client,
smtp_client,
};
run(state).await?;

Expand Down
7 changes: 6 additions & 1 deletion apps/backend/src/settings.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
use config::{Config, Environment};
use serde::Deserialize;

#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Deserialize)]
pub struct Settings {
pub database_url: String,
pub redis_url: String,
pub sentry_dsn: Option<String>,
#[serde(default = "default_allow_origin")]
pub allow_origin: String,
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_username: String,
pub smtp_password: String,
pub smtp_from: String,
Comment thread
sousuke0422 marked this conversation as resolved.
Comment on lines +11 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

SMTP設定を全必須にすると既存環境で起動不能になる可能性があります。

Line 11-15 の smtp_* がすべて必須のため、未設定環境では load_settings() のデシリアライズで即失敗します。段階的導入を想定するなら Option 化または feature flag で無効化可能にして、メール未使用時は起動継続できる形にしてください。

As per coding guidelines 「Rust / Axum / SeaORM のバックエンドです。認証・認可、テナント境界、SQL の正しさ、エラーハンドリング、非同期処理の安全性を優先して確認してください。」。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/settings.rs` around lines 11 - 15, The SMTP fields
(smtp_host, smtp_port, smtp_username, smtp_password, smtp_from) are currently
required and cause load_settings() to fail in environments without SMTP; change
these fields to optional (e.g., Option<String>/Option<u16>) or gate them behind
a feature flag so settings deserialization succeeds when SMTP is unused, update
load_settings() to accept/migrate optional values, and adjust any code that
references these fields (e.g., mail-sending helper functions) to handle None
(skip sending or return a clear error) and to validate presence only when SMTP
is actually enabled.

}

fn default_allow_origin() -> String {
Expand Down
5 changes: 4 additions & 1 deletion apps/backend/src/utils/auth.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use argon2::{
Argon2,
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::{OsRng, RngCore}},
password_hash::{
PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
rand_core::{OsRng, RngCore},
},
};

use axum::{
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod auth;
pub mod redis;
pub mod smtp;
110 changes: 110 additions & 0 deletions apps/backend/src/utils/smtp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! SMTPクライアントを提供するモジュール

use lettre::Tokio1Executor;
use lettre::message::{Mailbox, Message, MultiPart, SinglePart};
use lettre::{AsyncSmtpTransport, AsyncTransport, transport::smtp::authentication::Credentials};
use std::time::Duration;
use tokio::time::timeout;

/// SMTP送信タイムアウト(秒)
const SMTP_SEND_TIMEOUT_SECS: u64 = 30;

/// SMTPクライアントの構造体
#[derive(Clone)]
pub struct SmtpClient {
mailer: AsyncSmtpTransport<Tokio1Executor>,
}

/// SmtpClientの実装
impl SmtpClient {
/// 新しいSMTPクライアントを作成する関数
///
/// # Arguments
/// * `smtp_server` - SMTPサーバーのアドレス
/// * `smtp_port` - SMTPサーバーのポート番号
/// * `username` - SMTPサーバーの認証に使用するユーザー名
/// * `password` - SMTPサーバーの認証に使用するパスワード
///
/// # Examples
///
/// ```no_run
/// use backend::utils::smtp::SmtpClient;
///
/// let smtp_client = SmtpClient::new("smtp.example.com", 587, "user", "pass").unwrap();
/// ```
pub fn new(
smtp_server: &str,
smtp_port: u16,
username: &str,
password: &str,
) -> Result<Self, lettre::transport::smtp::Error> {
let creds = Credentials::new(username.to_string(), password.to_string());

let mailer = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(smtp_server)?
.port(smtp_port)
.credentials(creds)
.build();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Ok(SmtpClient { mailer })
}

/// メールを送信する関数
///
/// # Arguments
/// * `from` - 送信元のメールアドレス
/// * `to` - 送信先のメールアドレス
/// * `subject` - メールの件名
/// * `body_text` - メールのテキスト形式の本文
/// * `body_html` - メールのHTML形式の本文(オプション)
///
/// # Examples
///
/// ```no_run
/// use backend::utils::smtp::SmtpClient;
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = SmtpClient::new("smtp.example.com", 587, "user", "pass")?;
/// client.send_email(
/// "sender@example.com",
/// "receiver@example.com",
/// "Hello from Async Rust!",
/// "This is a test email sent asynchronously.",
/// Some("<p>This is a <b>test</b> email sent asynchronously.</p>"),
/// ).await?;
/// # Ok(())
/// # }
/// ```
pub async fn send_email(
&self,
from: &str,
to: &str,
subject: &str,
body_text: &str,
body_html: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
let builder = Message::builder()
.from(from.parse::<Mailbox>()?)
.to(to.parse::<Mailbox>()?)
.subject(subject);

let email = match body_html {
Some(html) => builder.multipart(
MultiPart::alternative()
.singlepart(SinglePart::plain(body_text.to_string()))
.singlepart(SinglePart::html(html.to_string())),
)?,
None => builder.singlepart(SinglePart::plain(body_text.to_string()))?,
};

// 送信処理にタイムアウトを30秒に設定
timeout(
Duration::from_secs(SMTP_SEND_TIMEOUT_SECS),
self.mailer.send(email),
)
.await
.map_err(|_| "SMTP send timeout")??;

Ok(())
}
}