From 18e74dd57e33f3da3e0a2589dc7ef21a52b3d5ea Mon Sep 17 00:00:00 2001 From: Andrea Toska Date: Sat, 26 Apr 2025 13:38:11 +0200 Subject: [PATCH] feat(email): add email verification --- .DS_Store | Bin 0 -> 6148 bytes ...acb2bc8c02735e2f79782c612d5fe0cf01042.json | 27 +++ ...6c61aaef9395170430a58dcd7ac78f8cc75b2.json | 22 ++ ...1c95230d4d5a7ee3cb66d9382b7f0522c5e82.json | 15 ++ ...c1c262351465b6f102e98912f67442a1f54d9.json | 20 ++ ...604095f71324011e7093ec37af15da8c158f4.json | 66 ++++++ ...3087943e5816a98cdce151302055c58bd1183.json | 28 +++ ...ce0b98ea2c7de03e71ac704c3ba047162c0b2.json | 28 +++ ...b3d302d083ec5333f3a5ce04113c38a041100.json | 130 +++++++++++ ...59bf11353f28c25e4f81268e9379b5b2cb375.json | 22 ++ ...52257c0ef596eee2d9a0e6a7d0aad03de4421.json | 130 +++++++++++ ...66fc2a97ac6e25a9733d1d20faf555c67abe1.json | 66 ++++++ ...c3bbd90d4bcf92d20203cba5afb2664f4bb8a.json | 22 ++ ...d15f9e4bafdfca12d2130a873d44a88dd435b.json | 22 ++ ...e47d6180ff634ebf59f3a1efd92b797170ac2.json | 30 +++ ...17f12ffeeb26a5eabca323abfed0e1d7bcbea.json | 28 +++ Cargo.lock | 123 +++++++++++ Cargo.toml | 4 +- res/.DS_Store | Bin 0 -> 6148 bytes res/email/confirmationTemplate.html | 203 ++++++++++++++++++ src/account/account.rs | 3 +- src/email.rs | 38 ++++ src/main.rs | 4 +- src/nnid/email.rs | 11 - src/nnid/mod.rs | 2 +- src/nnid/people.rs | 16 +- src/nnid/support.rs | 65 ++++++ 27 files changed, 1107 insertions(+), 18 deletions(-) create mode 100644 .DS_Store create mode 100644 .sqlx/query-02d51edf65163f311dfa215da26acb2bc8c02735e2f79782c612d5fe0cf01042.json create mode 100644 .sqlx/query-08c4a5721982ecd267ebc515b866c61aaef9395170430a58dcd7ac78f8cc75b2.json create mode 100644 .sqlx/query-248fc3dbfadb793f1f380486d9c1c95230d4d5a7ee3cb66d9382b7f0522c5e82.json create mode 100644 .sqlx/query-3c9b1695f8ae49e4308c048de98c1c262351465b6f102e98912f67442a1f54d9.json create mode 100644 .sqlx/query-48710e0b87742cc3fef816b3c95604095f71324011e7093ec37af15da8c158f4.json create mode 100644 .sqlx/query-566221b869f293e6c721e2e8bbf3087943e5816a98cdce151302055c58bd1183.json create mode 100644 .sqlx/query-5bd26d4c9e701bde77ce598fea2ce0b98ea2c7de03e71ac704c3ba047162c0b2.json create mode 100644 .sqlx/query-606364c79e0990deb07dfbe6c32b3d302d083ec5333f3a5ce04113c38a041100.json create mode 100644 .sqlx/query-6c1df0b05553305ba847f571a5859bf11353f28c25e4f81268e9379b5b2cb375.json create mode 100644 .sqlx/query-93960465bbf8f670891d49b95fc52257c0ef596eee2d9a0e6a7d0aad03de4421.json create mode 100644 .sqlx/query-9d3cee43a86cead9a6d078abc1266fc2a97ac6e25a9733d1d20faf555c67abe1.json create mode 100644 .sqlx/query-b16ba4b6c1b7d1c207e94515268c3bbd90d4bcf92d20203cba5afb2664f4bb8a.json create mode 100644 .sqlx/query-c8d50662530cac49c4261fb321cd15f9e4bafdfca12d2130a873d44a88dd435b.json create mode 100644 .sqlx/query-e5a2f7f28c3d7b9524d3dce48a9e47d6180ff634ebf59f3a1efd92b797170ac2.json create mode 100644 .sqlx/query-eafa97669e8ec04f1dc0a8e05d417f12ffeeb26a5eabca323abfed0e1d7bcbea.json create mode 100644 res/.DS_Store create mode 100644 res/email/confirmationTemplate.html create mode 100644 src/email.rs delete mode 100644 src/nnid/email.rs create mode 100644 src/nnid/support.rs diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..853c87a9256076cb997a4ecd7c1a237130e1e110 GIT binary patch literal 6148 zcmeHKO>fgM7=GQ9HQfa20i<1!B5@s~V~7cH$-4G132`6@4uDG1W+PgbR7t8Hs!BPJ z|G;11$}i!+aDwNxU6G`N6G902m0mxN{XYJ9T-!AfiNSn&K-4258_w9?MskgDJ^O~W zbjc1-$Tp_5PZ{N>aXQ(EHU&ljqrktW0PkI!0-94qF;(_&5T^m^_+U7O?|+wu9MLwN zkdJ+SPAT?#jC4X5yyq*N&%fmq;9OE%=ZL!-oS|x)LiBbfM;JCZ9~+!o7*$3yjOaen z41Dg3=Xa*grJrLI-x}NwJ)=|1ZA8z>qfx!5)_Eh^!VDsc>QPdwPZg~!j3I3HOsvi| zQNbdg>G6FDW^t4k{r*?cvf4McI(En2vEO+g(uG&}#XKMQ*&F_PnWk~D&ilcuC|rzt zw@=cf@S`NmBq5rH2zmQDN~Y;zkj|57F1ennu)B75+}m9)4dMH~tJt*u&2ne2*+U|-XUSh3>y&BPjE;VVwHkxEglTz4H$a(jx z2wwdJegF?1{Stlv{mq|3vQ@l@NSO!oelPRh%XEIPGm{}A)}KW^qBaq6G>NV2P`?oA zmu*e9OpgO(>K!p<6jM&+NxBwoJ1hg1f&YyGxpo~2=!~XVxwZ8xMqw&Kc;(jc1g-N! z2`r+08j-*{$4W;|0o|c7bVlh(+Q|HPC38oO5m0h9lHEn*m)aP$$Z%dv%&++rPST_( zyWOvBYrC~`ZI|!zJ$~ws#oRB0a#r+%>1+A)LWne6*+KX+iRZ)i&8H$SgCviqDj|tt zguHo`Bauy3?xr^rgMpbZKRW&X;r!cYS%F%9CZaz?OvY;? z+(3mAwL_n)>dDG==p8g_*{mD8jq&Ogul-PKZU_VBzsmPEWIlEpmI2Ga-(x`T4=$QS z&tRodZ5>eR3IJ@OTM2CW!60*7gPy@kBRmkMQ-L~_nJWg<>F9S&oM*7osMCp=%Lg+f zGjl^>YIMBcRpG=ujaFL*ECWpjw)JCMp8wB&fB$bL*(b|@WniNiV6A~a=wnLeY+ak2 wJZoLFw`fviT%}Q^K$+uM8S*GTKvM#rD-ED$u+oSgi2V>yG+1RB_@@l~1YdKSSO5S3 literal 0 HcmV?d00001 diff --git a/res/email/confirmationTemplate.html b/res/email/confirmationTemplate.html new file mode 100644 index 0000000..5387c5b --- /dev/null +++ b/res/email/confirmationTemplate.html @@ -0,0 +1,203 @@ + + + + + + + + +
Hello {{username}}! Your Splatfestival Network ID activation is almost complete. Please click the link in this email to confirm your e-mail address and complete the activation process.
+ + + + +
+ + + + +
+ + + + + + +
  + + + + + + + +
 
+ + + + + + + + + + + + + + + + + + + +
+ + + +
 
+ + + + + + +
  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ Hello {{username}}! +
 
+ Your Splatfestival Network ID activation is almost complete. +
 
 
+ Enter the following 6-digit code on your console: +
 
+ {{confirmation-code}} +
 
+ We hope you have fun using our services! +
 
+ The SPFN team +
 
+
 
+
 
+ Note: this email message was auto-generated, please do not respond. For further assistance, please join our Discord server or make a post on our Forum. +
 
+
+
 
+
+
+ + \ No newline at end of file diff --git a/src/account/account.rs b/src/account/account.rs index 6ceead2..e83a1be 100644 --- a/src/account/account.rs +++ b/src/account/account.rs @@ -54,7 +54,8 @@ pub struct User { pub mii_data: String, pub creation_date: NaiveDateTime, pub updated: NaiveDateTime, - pub nex_password: String + pub nex_password: String, + pub verification_code: Option } fn generate_nintendo_hash(pid: i32, text_password: &str) -> String{ diff --git a/src/email.rs b/src/email.rs new file mode 100644 index 0000000..255fe49 --- /dev/null +++ b/src/email.rs @@ -0,0 +1,38 @@ +use lettre::transport::smtp::authentication::Credentials; +use lettre::{Message, SmtpTransport, Transport}; +use std::env; +use std::fs; + +pub async fn send_verification_email(to: &str, code: i32, username: &str) -> Result<(), String> { + let smtp_user = env::var("SMTP_USER").map_err(|_| "SMTP_USER not set".to_string())?; + let smtp_pass = env::var("SMTP_PASS").map_err(|_| "SMTP_PASS not set".to_string())?; + let smtp_server = env::var("SMTP_SERVER").map_err(|_| "SMTP_SERVER not set".to_string())?; + + // Load template + let template = fs::read_to_string("res/email/confirmationTemplate.html") + .map_err(|e| format!("Failed to read email template: {}", e))?; + + // Replace placeholders + let body = template + .replace("{{username}}", username) + .replace("{{confirmation-code}}", &format!("{:06}", code)); + + let email = Message::builder() + .from(smtp_user.parse().unwrap()) + .to(to.parse().unwrap()) + .subject("Your Verification Code") + .header(lettre::message::header::ContentType::TEXT_HTML) + .body(body) + .map_err(|e| e.to_string())?; + + let creds = Credentials::new(smtp_user, smtp_pass); + + let mailer = SmtpTransport::relay(&smtp_server) + .map_err(|e| e.to_string())? + .credentials(creds) + .build(); + + mailer.send(&email).map_err(|e| e.to_string())?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 26a766a..071c5e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ mod data_wrapper; #[deprecated] mod grpc; mod graphql; +mod email; type Pool = sqlx::Pool; @@ -109,7 +110,8 @@ async fn launch() -> _ { nnid::agreements::get_agreement, nnid::timezones::get_timezone, nnid::person_exists::person_exists, - nnid::email::validate, + nnid::support::validate, + nnid::support::verify_email, nnid::people::create_account, nnid::people::get_own_profile, nnid::oauth::generate_token::generate_token, diff --git a/src/nnid/email.rs b/src/nnid/email.rs deleted file mode 100644 index a229bd6..0000000 --- a/src/nnid/email.rs +++ /dev/null @@ -1,11 +0,0 @@ -use rocket::{post, FromForm}; -use rocket::form::Form; - -#[derive(FromForm)] -struct ValidateEmailInput{ - email: String, -} -#[post("/v1/api/support/validate/email", data="")] -pub fn validate(data: Form){ - -} \ No newline at end of file diff --git a/src/nnid/mod.rs b/src/nnid/mod.rs index e0c1f2f..a86c6f7 100644 --- a/src/nnid/mod.rs +++ b/src/nnid/mod.rs @@ -2,9 +2,9 @@ pub mod devices; pub mod agreements; pub mod timezones; pub mod person_exists; -pub mod email; pub mod oauth; mod pid_distribution; pub mod people; pub mod provider; pub mod mapped_ids; +pub mod support; diff --git a/src/nnid/people.rs b/src/nnid/people.rs index 5450d77..3ae5895 100644 --- a/src/nnid/people.rs +++ b/src/nnid/people.rs @@ -18,6 +18,8 @@ use crate::nnid::pid_distribution::next_pid; use crate::nnid::timezones::{OFFSET_FROM_TIMEZONE, ZONE_TO_TIMEZONES}; use crate::Pool; use crate::xml::{Xml, YesNoVal}; +use crate::email::send_verification_email; +use rand::Rng; static S3_URL_STRING: Lazy> = Lazy::new(|| env::var("S3_URL").expect("S3_URL not specified").into_boxed_str() @@ -114,6 +116,8 @@ pub async fn create_account(database: &State, data: Xml, data: Xml, data: Xml){ + if let Err(e) = send_verification_email(&*data.email, 123456, "Andrea Test Username").await { + println!("Failed to send verification email: {e}"); + } +} + +#[get("/v1/api/support/email_confirmation//")] +pub async fn verify_email(database: &State, pid: i32, code: i32) -> Result<(), Errors<'static>> { + let db = database.inner(); + + let result = sqlx::query!( + "SELECT verification_code FROM users WHERE pid = $1", + pid + ) + .fetch_optional(db) + .await; + + let Ok(Some(record)) = result else { + return Err(BAD_CODE_ERROR); + }; + + if let Some(stored_code) = record.verification_code { + if stored_code == code { + // Set email_verified_since to NOW + let now = Utc::now().naive_utc(); + let update_result = sqlx::query!( + "UPDATE users SET email_verified_since = $1 WHERE pid = $2", + now, + pid + ) + .execute(db) + .await; + + if update_result.is_err() { + return Err(BAD_CODE_ERROR); // fallback in case the update fails + } + + return Ok(()); // Success + } + } + + Err(BAD_CODE_ERROR) +}