feat: add SMTP configuration and registration endpoint for email verification
This commit is contained in:
parent
8499636209
commit
d971a10cf4
3
.github/workflows/deploy.yml
vendored
3
.github/workflows/deploy.yml
vendored
@ -34,3 +34,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
shuttle-api-key: ${{ secrets.SHUTTLE_API_KEY }}
|
shuttle-api-key: ${{ secrets.SHUTTLE_API_KEY }}
|
||||||
project-id: proj_01JNH9KPMRS34FKC2NHWQ5YNNB
|
project-id: proj_01JNH9KPMRS34FKC2NHWQ5YNNB
|
||||||
|
secrets: |
|
||||||
|
SMTP_MAIL = '${{ secrets.SMTP_MAIL }}'
|
||||||
|
SMTP_SECRET = '${{ secrets.SMTP_SECRET }}'
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -26,5 +26,6 @@ Cargo.lock
|
|||||||
#/target
|
#/target
|
||||||
.shuttle/
|
.shuttle/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
Secrets.toml
|
||||||
|
Secrets.dev.toml
|
||||||
**/*.pdf
|
**/*.pdf
|
||||||
|
@ -5,9 +5,12 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.8.1"
|
axum = "0.8.1"
|
||||||
|
email_address = "0.2.9"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
shuttle-axum = "0.53.0"
|
shuttle-axum = "0.53.0"
|
||||||
shuttle-runtime = "0.53.0"
|
shuttle-runtime = "0.53.0"
|
||||||
tokio = "1.28.2"
|
tokio = "1.28.2"
|
||||||
tower-http = { version = "0.6.2", features = ["fs"] }
|
tower-http = { version = "0.6.2", features = ["fs"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
|
serde = { version = "1.0.203", features = ["derive"] }
|
||||||
|
lettre = { version = "0.11.15", default-features = false, features = ["builder", "smtp-transport", "ring", "rustls", "rustls-tls"] }
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
use crate::api::v1::{get_version, health_check};
|
use crate::api::v1::{get_version, health_check, onboarding};
|
||||||
|
use crate::model::config::SMTPConfig;
|
||||||
use axum::{routing, Router};
|
use axum::{routing, Router};
|
||||||
|
|
||||||
pub mod v1;
|
pub mod v1;
|
||||||
|
|
||||||
pub fn new() -> Router {
|
pub fn new(smtp_config: SMTPConfig) -> Router {
|
||||||
Router::new().nest(
|
Router::new().nest(
|
||||||
"/v1",
|
"/v1",
|
||||||
Router::new()
|
onboarding::new(smtp_config)
|
||||||
.route("/version", routing::get(get_version))
|
.route("/version", routing::get(get_version))
|
||||||
.route("/health", routing::get(health_check)),
|
.route("/health", routing::get(health_check)),
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
mod health;
|
mod health;
|
||||||
|
pub mod onboarding;
|
||||||
mod version;
|
mod version;
|
||||||
|
|
||||||
pub use health::health_check;
|
pub use health::health_check;
|
||||||
|
12
src/api/v1/onboarding.rs
Normal file
12
src/api/v1/onboarding.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
mod registration;
|
||||||
|
|
||||||
|
use crate::api::v1::onboarding::registration::register;
|
||||||
|
use crate::config::SMTPConfig;
|
||||||
|
use axum::{routing, Router};
|
||||||
|
|
||||||
|
pub fn new(smtp_config: SMTPConfig) -> Router {
|
||||||
|
Router::new().nest(
|
||||||
|
"/onboarding",
|
||||||
|
Router::new().route("/register", routing::post(register).with_state(smtp_config)),
|
||||||
|
)
|
||||||
|
}
|
66
src/api/v1/onboarding/registration.rs
Normal file
66
src/api/v1/onboarding/registration.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
use crate::config::SMTPConfig;
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::Json;
|
||||||
|
use lettre::message::header::ContentType;
|
||||||
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tracing::{error, info, instrument, warn};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Registration {
|
||||||
|
email_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(mail, smtp))]
|
||||||
|
pub async fn register(
|
||||||
|
State(smtp): State<SMTPConfig>,
|
||||||
|
Json(mail): Json<Registration>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let email = match email_address::EmailAddress::from_str(&mail.email_address) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
let err = format!("Invalid email address: {e}");
|
||||||
|
warn!(email = mail.email_address, error = %err);
|
||||||
|
return (StatusCode::BAD_REQUEST, err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!(email = mail.email_address, "RegistrationRequest received");
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
let verification_email = Message::builder()
|
||||||
|
.from(
|
||||||
|
format!("Digitaler Frieden <{}>", smtp.mail())
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.to(email.to_string().parse().unwrap())
|
||||||
|
.subject("Verify your email address")
|
||||||
|
.header(ContentType::TEXT_HTML)
|
||||||
|
.body(String::from("<h1>Verify your Email Address!</h1>"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let credentials = Credentials::new(smtp.mail().to_string(), smtp.secret().to_string());
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||||
|
.unwrap()
|
||||||
|
.credentials(credentials)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
match mailer.send(&verification_email) {
|
||||||
|
Ok(_) => {
|
||||||
|
info!(email = mail.email_address, "Verification email sent");
|
||||||
|
(StatusCode::OK, "Verification email sent".to_string())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let err = format!("Failed to send verification email: {e}");
|
||||||
|
error!(error = %err);
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/lib.rs
16
src/lib.rs
@ -1,13 +1,19 @@
|
|||||||
mod api;
|
mod api;
|
||||||
|
mod model;
|
||||||
|
|
||||||
|
pub use model::config;
|
||||||
|
|
||||||
|
use crate::model::config::SMTPConfig;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use tower_http::services::{ServeDir, ServeFile};
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
|
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
pub fn new() -> Router {
|
pub fn new(smtp_config: SMTPConfig) -> Router {
|
||||||
Router::new().nest("/api", api::new()).fallback_service(
|
Router::new()
|
||||||
ServeDir::new("frontend/dist/frontend/browser")
|
.nest("/api", api::new(smtp_config))
|
||||||
.not_found_service(ServeFile::new("frontend/dist/frontend/browser/index.html")),
|
.fallback_service(
|
||||||
)
|
ServeDir::new("frontend/dist/frontend/browser")
|
||||||
|
.not_found_service(ServeFile::new("frontend/dist/frontend/browser/index.html")),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
11
src/main.rs
11
src/main.rs
@ -1,6 +1,13 @@
|
|||||||
|
use shuttle_runtime::SecretStore;
|
||||||
|
|
||||||
#[shuttle_runtime::main]
|
#[shuttle_runtime::main]
|
||||||
#[allow(clippy::unused_async)]
|
#[allow(clippy::unused_async)]
|
||||||
async fn main() -> shuttle_axum::ShuttleAxum {
|
async fn main(#[shuttle_runtime::Secrets] secrets: SecretStore) -> shuttle_axum::ShuttleAxum {
|
||||||
let router = digitaler_frieden::new();
|
#[allow(clippy::expect_used)]
|
||||||
|
let smtp_config = digitaler_frieden::config::SMTPConfig::new(
|
||||||
|
&secrets.get("SMTP_MAIL").expect("SMTP_MAIL not set"),
|
||||||
|
&secrets.get("SMTP_SECRET").expect("SMTP_SECRET not set"),
|
||||||
|
);
|
||||||
|
let router = digitaler_frieden::new(smtp_config);
|
||||||
Ok(router.into())
|
Ok(router.into())
|
||||||
}
|
}
|
||||||
|
1
src/model.rs
Normal file
1
src/model.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod config;
|
2
src/model/config.rs
Normal file
2
src/model/config.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
mod smtp_config;
|
||||||
|
pub use smtp_config::SMTPConfig;
|
36
src/model/config/smtp_config.rs
Normal file
36
src/model/config/smtp_config.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct SMTPConfig {
|
||||||
|
mail: String,
|
||||||
|
secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SMTPConfig {
|
||||||
|
pub fn new<T: AsRef<str>>(mail: T, secret: T) -> Self {
|
||||||
|
Self {
|
||||||
|
mail: mail.as_ref().to_owned(),
|
||||||
|
secret: secret.as_ref().to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn mail(&self) -> &str {
|
||||||
|
&self.mail
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn secret(&self) -> &str {
|
||||||
|
&self.secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for SMTPConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("SMTPConfig")
|
||||||
|
.field("mail", &self.mail)
|
||||||
|
.field("secret", &"<reducted>")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user