230 lines
No EOL
5.9 KiB
Rust
230 lines
No EOL
5.9 KiB
Rust
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<Self, Self::Error> {
|
|
match req.headers().get_one("x-nintendo-pid") {
|
|
Some(val) => match val.parse::<i32>() {
|
|
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 = "<data>")]
|
|
pub async fn validate(
|
|
data: Form<ValidateEmailInput>,
|
|
) -> 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/<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);
|
|
};
|
|
|
|
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<Pool>,
|
|
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/<pid>")]
|
|
pub async fn forgotten_password(
|
|
database: &State<Pool>,
|
|
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(())
|
|
} |