feat(email): add email verification
This commit is contained in:
parent
1192a58342
commit
18e74dd57e
27 changed files with 1107 additions and 18 deletions
|
|
@ -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<i32>
|
||||
}
|
||||
|
||||
fn generate_nintendo_hash(pid: i32, text_password: &str) -> String{
|
||||
|
|
|
|||
38
src/email.rs
Normal file
38
src/email.rs
Normal file
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ mod data_wrapper;
|
|||
#[deprecated]
|
||||
mod grpc;
|
||||
mod graphql;
|
||||
mod email;
|
||||
|
||||
type Pool = sqlx::Pool<Postgres>;
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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="<data>")]
|
||||
pub fn validate(data: Form<ValidateEmailInput>){
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Box<str>> = 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<Pool>, data: Xml<AccountCreationDat
|
|||
|
||||
let pid = next_pid(database).await;
|
||||
|
||||
let verification_code: i32 = rand::thread_rng().gen_range(100_000..1_000_000);
|
||||
|
||||
let AccountCreationData {
|
||||
user_id,
|
||||
password,
|
||||
|
|
@ -152,9 +156,10 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
|
|||
off_device_allowed,
|
||||
region,
|
||||
gender,
|
||||
mii_data
|
||||
mii_data,
|
||||
verification_code
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13
|
||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14
|
||||
)
|
||||
",
|
||||
pid,
|
||||
|
|
@ -169,11 +174,16 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
|
|||
off_device_flag.0,
|
||||
region,
|
||||
gender.as_ref(),
|
||||
data.as_ref()
|
||||
data.as_ref(),
|
||||
verification_code,
|
||||
).execute(database).await.unwrap();
|
||||
|
||||
generate_s3_images(pid, &data).await;
|
||||
|
||||
if let Err(e) = send_verification_email(address.as_ref(), verification_code, user_id.as_ref()).await {
|
||||
println!("Failed to send verification email: {e}");
|
||||
}
|
||||
|
||||
Ok(
|
||||
Xml(AccountCreationResponseData{
|
||||
pid
|
||||
|
|
|
|||
65
src/nnid/support.rs
Normal file
65
src/nnid/support.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
use rocket::{get, State, post, FromForm};
|
||||
use rocket::http::Status;
|
||||
use crate::Pool;
|
||||
use rocket::form::Form;
|
||||
use crate::email::send_verification_email;
|
||||
use crate::error::{Error, Errors};
|
||||
use chrono::Utc;
|
||||
|
||||
const BAD_CODE_ERROR: Errors = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
code: "0116",
|
||||
message: "Missing or invalid verification code"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct ValidateEmailInput{
|
||||
email: String,
|
||||
}
|
||||
#[post("/v1/api/support/validate/email", data="<data>")]
|
||||
pub async fn validate(data: Form<ValidateEmailInput>){
|
||||
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/<pid>/<code>")]
|
||||
pub async fn verify_email(database: &State<Pool>, 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue