use crate::Pool; use crate::error::{Error, Errors}; use chrono::Utc; use hickory_resolver::TokioAsyncResolver; use rocket::form::Form; use rocket::{FromForm, State, post, put, get, Request}; use rocket::request::{self, FromRequest}; use rocket::http::Status; use rand::RngExt; use rand::distr::Alphanumeric; const BAD_CODE_ERROR: Errors = Errors { error: &[Error { code: "0116", message: "Missing or invalid verification code", }], }; const UNAUTHORIZED_DEVICE_ERROR: Errors = Errors { error: &[Error { code: "0113", message: "Unauthorized device", }], }; #[derive(FromForm)] pub struct ValidateEmailInput { email: String, } // sorry maple i really tried and couldn't find a better way pub struct EmailPid(pub i32); #[rocket::async_trait] impl<'r> FromRequest<'r> for EmailPid { type Error = Errors<'static>; async fn from_request(req: &'r Request<'_>) -> request::Outcome { match req.headers().get_one("x-nintendo-pid") { Some(val) => match val.parse::() { Ok(parsed_id) => request::Outcome::Success(EmailPid(parsed_id)), Err(_) => request::Outcome::Error((Status::BadRequest, BAD_CODE_ERROR)), }, None => request::Outcome::Error((Status::BadRequest, BAD_CODE_ERROR)), } } } // i was tired at 1 am ok have mercy #[post("/v1/api/support/validate/email", data = "")] pub async fn validate( data: Form, ) -> Result<(), Errors<'static>> { let email = data.email.trim(); if email.is_empty() || !email.contains('@') { return Err(Errors { error: &[Error { code: "0103", message: "Email format is invalid", }], }); } let domain = match email.split('@').nth(1) { Some(d) if !d.is_empty() => d, _ => { return Err(Errors { error: &[Error { code: "0103", message: "Email format is invalid", }], }); } }; // This shouldn't ever fail unless there's something wrong with the server let resolver = TokioAsyncResolver::tokio_from_system_conf() .map_err(|_| Errors { error: &[Error { code: "1126", message: "DNS resolver initialization failed", }], })?; match resolver.mx_lookup(domain).await { Ok(mx) if mx.iter().next().is_some() => Ok(()), _ => Err(Errors { error: &[Error { code: "1126", message: "The domain is not accessible", }], }), } } #[put("/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); }; let 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) } #[get("/v1/api/support/resend_confirmation")] pub async fn resend_email( database: &State, pid: EmailPid, ) -> Result<(), Errors<'static>> { let pid = pid.0; let user_data = sqlx::query!( "SELECT username, verification_code, email FROM users WHERE pid = $1", pid ) .fetch_optional(&**database) .await .map_err(|e| { eprintln!("{}", e); BAD_CODE_ERROR })?; let user = match user_data { Some(row) => row, None => return Err(BAD_CODE_ERROR), }; crate::email::send_verification_email(&user.email, user.verification_code, &user.username) .await .map_err(|e| { eprintln!("Failed to send email: {}", e); BAD_CODE_ERROR })?; Ok(()) } #[get("/v1/api/support/forgotten_password/")] pub async fn forgotten_password( database: &State, pid: i32, ) -> Result<(), Errors<'static>> { let db = database.inner(); let user_data = sqlx::query!( "SELECT username, email FROM users WHERE pid = $1", pid ) .fetch_optional(db) .await .map_err(|e| { eprintln!("database lookup error: {e}"); UNAUTHORIZED_DEVICE_ERROR })?; let user = match user_data { Some(u) => u, None => return Err(UNAUTHORIZED_DEVICE_ERROR), }; let cleartext_password: String = rand::rng() .sample_iter(&Alphanumeric) .take(10) .map(char::from) .collect(); let nintendo_hash = crate::account::account::generate_password(pid, &cleartext_password) .ok_or(UNAUTHORIZED_DEVICE_ERROR)?; let hashed_password = bcrypt::hash(nintendo_hash, 10) .map_err(|e| { eprintln!("bcrypt error: {e}"); UNAUTHORIZED_DEVICE_ERROR })?; let update_result = sqlx::query!( "UPDATE users SET password = $1 WHERE pid = $2", hashed_password, pid ) .execute(db) .await; if let Err(e) = update_result { eprintln!("failed to update password for PID {pid}: {e}"); return Err(UNAUTHORIZED_DEVICE_ERROR); } crate::email::send_reset_email(&user.email, &cleartext_password, &user.username) .await .map_err(|e| { eprintln!("failed to send reset email: {e}"); UNAUTHORIZED_DEVICE_ERROR })?; Ok(()) }