feat: add SMTP configuration and registration endpoint for email verification
This commit is contained in:
parent
8499636209
commit
d971a10cf4
5
.github/workflows/deploy.yml
vendored
5
.github/workflows/deploy.yml
vendored
@ -33,4 +33,7 @@ jobs:
|
||||
- uses: shuttle-hq/deploy-action@v2
|
||||
with:
|
||||
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
|
||||
.shuttle/
|
||||
node_modules/
|
||||
|
||||
Secrets.toml
|
||||
Secrets.dev.toml
|
||||
**/*.pdf
|
||||
|
@ -5,9 +5,12 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8.1"
|
||||
email_address = "0.2.9"
|
||||
serde_json = "1.0.140"
|
||||
shuttle-axum = "0.53.0"
|
||||
shuttle-runtime = "0.53.0"
|
||||
tokio = "1.28.2"
|
||||
tower-http = { version = "0.6.2", features = ["fs"] }
|
||||
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};
|
||||
|
||||
pub mod v1;
|
||||
|
||||
pub fn new() -> Router {
|
||||
pub fn new(smtp_config: SMTPConfig) -> Router {
|
||||
Router::new().nest(
|
||||
"/v1",
|
||||
Router::new()
|
||||
onboarding::new(smtp_config)
|
||||
.route("/version", routing::get(get_version))
|
||||
.route("/health", routing::get(health_check)),
|
||||
)
|
||||
|
@ -1,4 +1,5 @@
|
||||
mod health;
|
||||
pub mod onboarding;
|
||||
mod version;
|
||||
|
||||
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 model;
|
||||
|
||||
pub use model::config;
|
||||
|
||||
use crate::model::config::SMTPConfig;
|
||||
use axum::Router;
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub fn new() -> Router {
|
||||
Router::new().nest("/api", api::new()).fallback_service(
|
||||
ServeDir::new("frontend/dist/frontend/browser")
|
||||
.not_found_service(ServeFile::new("frontend/dist/frontend/browser/index.html")),
|
||||
)
|
||||
pub fn new(smtp_config: SMTPConfig) -> Router {
|
||||
Router::new()
|
||||
.nest("/api", api::new(smtp_config))
|
||||
.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]
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn main() -> shuttle_axum::ShuttleAxum {
|
||||
let router = digitaler_frieden::new();
|
||||
async fn main(#[shuttle_runtime::Secrets] secrets: SecretStore) -> shuttle_axum::ShuttleAxum {
|
||||
#[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())
|
||||
}
|
||||
|
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