implement missing routes
Some checks failed
Build and Test / account (push) Failing after 52s

This commit is contained in:
red binder 2026-08-10 21:39:11 +02:00
commit 672674f7ad
10 changed files with 639 additions and 31 deletions

View file

@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE users SET\n email = $1,\n email_verified_since = NULL,\n verification_code = $2\n WHERE pid = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "0edf3f60bfb92912f73b6c61962e4a98b37c62d596cb0c8c3d1cc1c0199dcb2d"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE users SET\n gender = COALESCE($1, gender),\n region = COALESCE($2, region),\n country = COALESCE($3, country),\n language = COALESCE($4, language),\n timezone = COALESCE($5, timezone),\n marketing_allowed = COALESCE($6, marketing_allowed),\n off_device_allowed = COALESCE($7, off_device_allowed),\n password = COALESCE($8, password)\n WHERE pid = $9\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bpchar",
"Int4",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Bool",
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "8cd49b3defe144fe73bdeee1ce000b9bb8716320c6b421395a04f12bd826a469"
}

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM users WHERE pid = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": []
},
"hash": "ffef185a4db4b101477f7a78444192a7dfa8af98c00394978a9627b5ecdae9b5"
}

View file

@ -1 +1 @@
<?xml version="1.0"?><agreements><agreement><country>US</country><language>en</language><language_name>English</language_name><publish_date>2014-09-29T20:07:35</publish_date><texts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="chunkedStoredAgreementText"><main_title><![CDATA[Splatfestival Network Services Agreement]]></main_title><agree_text><![CDATA[I Accept]]></agree_text><non_agree_text><![CDATA[I Decline]]></non_agree_text><main_text index="1"><![CDATA[Welcome to the Splatfestival Network! If you are seeing this, you have correctly installed the environment! Please note that we have rules to follow. You only have one warning. The rules are the following: Do not harrass people. Do not advertise your own servers and such. Do not use ANY hacks. That includes Silverlight, you will be banned. Do not try and stress test the server. Do not DDoS the server. If we detect a pirated copy of Splatoon, you will be banned without appeal. Do not impersonate staff members. If you have any questions, please contact TV/Maple(username: djt.v.) on discord.]]></main_text><sub_title><![CDATA[SPFN Privacy Policy]]></sub_title><sub_text index="1"><![CDATA[Please note that we will store the following: Email Address, IP Address, birthdate and timezone. These are required for the following purposes: Email is required to validate you as a real person. It will only be stored for the purpose of sending you a validation email. Your IP address is required to make sure you do not bypass any bans and store your current connection to the server so that you cannot connect twice. Your birthdate is required to make sure you are old enough to access our services and your timezone is required to have a valid created date for your account.]]></sub_text></texts><type>NINTENDO-NETWORK-EULA</type><version>0300</version></agreement><agreement><country>US</country><language>en</language><language_name>Español</language_name><publish_date>2014-09-29T20:07:35</publish_date><texts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="chunkedStoredAgreementText"><main_title><![CDATA[Acuerdo de servicios de red de Splatfestival]]></main_title><agree_text><![CDATA[I Accept]]></agree_text><non_agree_text><![CDATA[I Decline]]></non_agree_text><main_text index="1"><![CDATA[¡Bienvenido a la red Splatfestival! Si ves esto, ¡has instalado el entorno correctamente! Ten en cuenta que tenemos reglas que seguir. Solo tienes una advertencia. Las reglas son las siguientes: No acoses a la gente. No hagas publicidad de tus propios servidores ni nada parecido. No uses NINGÚN hack. Eso incluye Silverlight, serás baneado. No intentes poner a prueba el servidor. No hagas DDoS en el servidor. Si detectamos una copia pirateada de Splatoon, serás baneado sin posibilidad de apelación. No te hagas pasar por miembros del personal. Si tienes alguna pregunta, ponte en contacto con djt.v. en discord.]]></main_text><sub_title><![CDATA[Política de Privacidad]]></sub_title><sub_text index="1"><![CDATA[Tenga en cuenta que almacenaremos lo siguiente: d* Connection #0 to host account.spfn.cc left intactirección de correo electrónico, dirección IP, fecha de nacimiento y zona horaria. Estos son necesarios para los siguientes fines: el correo electrónico es necesario para validarlo como una persona real. Solo se almacenará con el fin de enviarle un correo electrónico de validación. Su dirección IP es necesaria para asegurarnos de que no eluda ninguna prohibición y para almacenar su conexión actual al servidor para que no pueda conectarse dos veces. Su fecha de nacimiento es necesaria para asegurarnos de que tiene la edad suficiente para acceder a nuestros servicios y su zona horaria es necesaria para tener una fecha de creación válida para su cuenta.]]></sub_text></texts><type>NINTENDO-NETWORK-EULA</type><version>0300</version></agreement></agreements>
<?xml version="1.0"?><agreements><agreement><country>US</country><language>en</language><language_name>English</language_name><publish_date>2014-09-29T20:07:35</publish_date><texts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="chunkedStoredAgreementText"><main_title><![CDATA[Splatfestival Network Services Agreement]]></main_title><agree_text><![CDATA[I Accept]]></agree_text><non_agree_text><![CDATA[I Decline]]></non_agree_text><main_text index="1"><![CDATA[Welcome to the Splatfestival Network! If you are seeing this, you have correctly installed the environment! Please note that we have rules to follow. You only have one warning. The rules are the following: Do not harrass people. Do not advertise your own servers and such. Do not use ANY hacks. That includes Silverlight, you will be banned. Do not try and stress test the server. Do not DDoS the server. If we detect a pirated copy of Splatoon, you will be banned without appeal. Do not impersonate staff members. If you have any questions, please contact TV/Maple(username: djt.v.) on discord.]]></main_text><sub_title><![CDATA[SPFN Privacy Policy]]></sub_title><sub_text index="1"><![CDATA[Please note that we will store the following: Email Address, IP Address, birthdate and timezone. These are required for the following purposes: Email is required to validate you as a real person. It will only be stored for the purpose of sending you a validation email. Your IP address is required to make sure you do not bypass any bans and store your current connection to the server so that you cannot connect twice. Your birthdate is required to make sure you are old enough to access our services and your timezone is required to have a valid created date for your account.]]></sub_text></texts><type>NINTENDO-NETWORK-EULA</type><version>0300</version></agreement><agreement><country>US</country><language>en</language><language_name>Español</language_name><publish_date>2014-09-29T20:07:35</publish_date><texts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="chunkedStoredAgreementText"><main_title><![CDATA[Acuerdo de servicios de red de Splatfestival]]></main_title><agree_text><![CDATA[I Accept]]></agree_text><non_agree_text><![CDATA[I Decline]]></non_agree_text><main_text index="1"><![CDATA[¡Bienvenido a la red Splatfestival! Si ves esto, ¡has instalado el entorno correctamente! Ten en cuenta que tenemos reglas que seguir. Solo tienes una advertencia. Las reglas son las siguientes: No acoses a la gente. No hagas publicidad de tus propios servidores ni nada parecido. No uses NINGÚN hack. Eso incluye Silverlight, serás baneado. No intentes poner a prueba el servidor. No hagas DDoS en el servidor. Si detectamos una copia pirateada de Splatoon, serás baneado sin posibilidad de apelación. No te hagas pasar por miembros del personal. Si tienes alguna pregunta, ponte en contacto con djt.v. en discord.]]></main_text><sub_title><![CDATA[Política de Privacidad]]></sub_title><sub_text index="1"><![CDATA[Tenga en cuenta que almacenaremos lo siguiente: dirección de correo electrónico, dirección IP, fecha de nacimiento y zona horaria. Estos son necesarios para los siguientes fines: el correo electrónico es necesario para validarlo como una persona real. Solo se almacenará con el fin de enviarle un correo electrónico de validación. Su dirección IP es necesaria para asegurarnos de que no eluda ninguna prohibición y para almacenar su conexión actual al servidor para que no pueda conectarse dos veces. Su fecha de nacimiento es necesaria para asegurarnos de que tiene la edad suficiente para acceder a nuestros servicios y su zona horaria es necesaria para tener una fecha de creación válida para su cuenta.]]></sub_text></texts><type>NINTENDO-NETWORK-EULA</type><version>0300</version></agreement></agreements>

View file

@ -38,7 +38,7 @@
color: #B60000 !important;
}
img.logo {
content: url("https://spfn.net/res/img/spfn.png") !important;
content: url("https://spfn.spbr.net/res/img/spfn.png") !important;
}
}
@media (prefers-color-scheme: dark) {
@ -91,8 +91,8 @@
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td>
<a href="https://spfn.net">
<img class="logo" width="auto" height="48px" src="https://spfn.net/res/img/spfn.png" alt="SPFN">
<a href="https://spfn.spbr.net">
<img class="logo" width="auto" height="48px" src="https://spfn.spbr.net/res/img/spfn.png" alt="SPFN">
</a>
</td>
</tr>
@ -122,19 +122,6 @@
Your Splatfestival Network ID activation is almost complete.
</td>
</tr>
<tr>
<td width="100%" height="16px" style="line-height: 16px;">&nbsp;</td>
</tr>
<!-- <tr>-->
<!-- <td class="confirm-link" bgcolor="#673db6" style="font-size: 14px; font-weight: 700; border-radius: 10px; padding: 12px" align="center">-->
<!-- <a href="{{confirmation-href}}" style="text-decoration: none; color: #ffffff; " width="100%">-->
<!-- Confirm email address-->
<!-- </a>-->
<!-- </td>-->
<!-- </tr>-->
<tr>
<td width="100%" height="48px" style="line-height: 48px;">&nbsp;</td>
</tr>
<tr>
<td>
Enter the following 6-digit code on your console:

View file

@ -0,0 +1,190 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" lang="en">
<html lang="en">
<head>
<meta name="color-scheme" content="light dark">
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap');
:root {
color-scheme: light dark;
supported-color-schemes:light dark;
}
@media (prefers-color-scheme: light) {
body.email-body,
table.centerer,
table.wrapper {
background-color: #FFFFFF !important;
color: #000000 !important;
}
table.card {
background-color: #B60000 !important;
}
span.shoutout {
color: #FFB3B3 !important;
}
td.confirm-link {
background-color: #FF4D4D !important;
}
td.confirm-code {
background-color: #FFB3B3 !important;
color: #660000 !important;
}
td.notice {
color: #FF4D4D !important;
}
td.notice a {
color: #B60000 !important;
}
img.logo {
content: url("https://spfn.spbr.net/res/img/spfn.png") !important;
}
}
@media (prefers-color-scheme: dark) {
body.email-body,
table.centerer,
table.wrapper {
background-color: #3B1B1B !important;
color: #FFFFFF !important;
}
table.card {
background-color: #4A2323 !important;
}
span.shoutout {
color: #FF9999 !important;
}
td.confirm-link {
background-color: #B60000 !important;
}
td.confirm-code {
background-color: #652323 !important;
color: #ffffff !important;
}
td.notice {
color: #C18989 !important;
}
td.notice a {
color: #F5C1C1 !important;
}
}
</style>
</head>
<body class="email-body" bgcolor="#1B1F3B" style="margin-left: 0; margin-right: 0; margin-top: 0; margin-bottom: 0; padding-left: 0; padding-right: 0; padding-top: 0; padding-bottom: 0; font-family: Poppins, Arial, Helvetica, sans-serif;">
<div style="display:none;">Hello {{username}}. Your Splatfestival Network ID password reset has been processed. You may use the temporary password below to log into your account.</div>
<table class="centerer" bgcolor="#1B1F3B" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td align="center">
<table class="wrapper" bgcolor="#1B1F3B" style="font-family: Poppins, Arial, Helvetica, sans-serif;" border="0" cellpadding="0" cellspacing="0" height="100%" width="420px">
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td width="32px">&nbsp;</td>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td height="36px" style="line-height: 36px;" width="100%">&nbsp;</td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td>
<a href="https://spfn.spbr.net">
<img class="logo" width="auto" height="48px" src="https://spfn.spbr.net/res/img/spfn.png" alt="SPFN">
</a>
</td>
</tr>
<tr>
<td width="100%" height="36px" style="line-height: 36px;">&nbsp;</td>
</tr>
<tr>
<td>
<table class="card" bgcolor="#23274a" style="color: #ffffff; border-radius: 10px;" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td width="24px" height="100%">&nbsp;</td>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr width="100%" height="48px" style="line-height: 48px;">
<td>&nbsp;</td>
</tr>
<tr style="font-size: 24px; font-weight: 700;">
<td>
Hello <span class="shoutout" style="color: #cab1fb;">{{username}}</span>.
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&nbsp;</td>
</tr>
<tr>
<td style="color: #ffffff; ">
Your Splatfestival Network ID password reset has been processed.
</td>
</tr>
<tr>
<td>
You may use the following temporary password to login:
</td>
</tr>
<tr>
<td width="100%" height="16px" style="line-height: 16px;">&nbsp;</td>
</tr>
<tr>
<td class="confirm-code" bgcolor="#373c65" style="color: #ffffff; font-size: 14px; font-weight: 700; border-radius: 10px; padding: 12px" align="center">
{{password}}
</td>
</tr>
<tr>
<td width="100%" height="48px" style="line-height: 48px;">&nbsp;</td>
</tr>
<tr>
<td>
Please change your password as soon as you can on your Wii U. (Click your mii on the top-left of the Wii U Menu and scroll to "Change Password")
</td>
</tr>
<tr>
<td width="100%" height="36px" style="line-height: 36px;">&nbsp;</td>
</tr>
<tr>
<td align="right">
The SPFN team
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&nbsp;</td>
</tr>
</table>
</td>
<td width="24px" height="100%">&nbsp;</td>
</tr>
</table>
</td>
</tr>
<tr>
<td width="100%" height="18px" style="line-height: 18px;">&nbsp;</td>
</tr>
<tr>
<td class="notice" style="color: #8990c1; font-size: 12px;">
Note: this email message was auto-generated, please do not respond. For further assistance, please join our <a href="https://discord.gg/grMSxZf" style="text-decoration: none; color: #ffffff; ">Discord server</a>.
</td>
</tr>
<tr>
<td width="100%" height="48px" style="line-height: 48px;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
<td width="32px">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View file

@ -36,3 +36,37 @@ pub async fn send_verification_email(to: &str, code: i32, username: &str) -> Res
Ok(())
}
pub async fn send_reset_email(to: &str, pwd: &str, 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/resetTemplate.html")
.map_err(|e| format!("Failed to read email template: {}", e))?;
// Replace placeholders
let body = template
.replace("{{username}}", username)
.replace("{{password}}", pwd);
let email = Message::builder()
.from(smtp_user.parse().unwrap())
.to(to.parse().unwrap())
.subject("Password Reset for SPFN")
.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(())
}

View file

@ -101,11 +101,19 @@ async fn launch() -> _ {
nnid::support::validate,
nnid::support::verify_email,
nnid::support::resend_email,
nnid::support::forgotten_password,
nnid::people::create_account,
nnid::people::get_own_profile,
nnid::people::get_device_owner,
nnid::people::get_own_device,
nnid::people::change_mii,
nnid::people::update_account,
nnid::people::delete_account,
nnid::people::get_user_devices,
nnid::people::get_device_status,
nnid::people::inactivate_current_device,
nnid::people::get_own_emails,
nnid::people::update_primary_email,
nnid::miis::get_miis,
nnid::oauth::generate_token::generate_token,
nnid::provider::get_nex_token,

View file

@ -13,6 +13,7 @@ use nex_account::grpc::{ActStageInfo, ActStageReturn};
use nex_account::grpc_client;
use rand::prelude::*;
use rocket::serde::{Deserialize, Serialize};
use rocket::request::{FromRequest, Outcome, Request};
use rocket::{State, get, post, put};
const DATABASE_ERROR: Errors = Errors {
@ -64,6 +65,67 @@ pub struct AccountCreationResponseData {
pid: i32,
}
#[derive(Serialize)]
#[serde(rename = "device")]
pub struct DeviceInfo {
pub device_id: String,
pub language: String,
pub updated: NaiveDateTime,
pub pid: i32,
pub platform_id: String,
pub region: String,
pub serial_number: String,
pub status: String,
pub system_version: String,
pub r#type: String,
pub updated_by: String,
}
#[derive(Serialize)]
pub struct DevicesWrapper {
#[serde(rename = "device")]
pub devices: Vec<DeviceInfo>,
}
#[derive(Serialize)]
pub struct EmptyDeviceResponse {
pub device: String,
}
#[derive(Deserialize)]
#[serde(rename = "person")]
pub struct UpdateAccountData {
pub gender: Option<Box<str>>,
pub region: Option<i32>,
pub country: Option<Box<str>>,
pub language: Option<Box<str>>,
pub tz_name: Option<Box<str>>,
pub marketing_flag: Option<YesNoVal>,
pub off_device_flag: Option<YesNoVal>,
pub password: Option<Box<str>>,
}
#[derive(Serialize)]
pub struct EmailWrapper {
pub email: EmailInfoOwnProfileData,
}
#[derive(Serialize)]
pub struct EmailsWrapper {
#[serde(rename = "email")]
pub emails: Vec<EmailInfoOwnProfileData>,
}
#[derive(Deserialize)]
pub struct UpdateEmailData {
pub address: Box<str>,
}
#[derive(Deserialize)]
pub struct UpdateEmailRequest {
pub email: UpdateEmailData,
}
#[post("/v1/api/people", data = "<data>")]
pub async fn create_account(
database: &State<Pool>,
@ -174,17 +236,17 @@ pub async fn create_account(
// }
#[derive(Serialize)]
struct EmailInfoOwnProfileData {
address: String,
id: u32,
parent: YesNoVal,
primary: YesNoVal,
reachable: YesNoVal,
pub struct EmailInfoOwnProfileData {
pub address: String,
pub id: u32,
pub parent: YesNoVal,
pub primary: YesNoVal,
pub reachable: YesNoVal,
#[serde(rename = "type")]
email_type: String,
updated_by: String,
validated: YesNoVal,
validated_date: Option<NaiveDateTime>,
pub email_type: String,
pub updated_by: String,
pub validated: YesNoVal,
pub validated_date: Option<NaiveDateTime>,
}
#[derive(Serialize)]
@ -528,3 +590,211 @@ pub async fn thing(
Ok(())
}
pub struct DeviceHeaders {
pub device_id: String,
pub accept_language: String,
pub platform_id: String,
pub region: String,
pub serial_number: String,
pub system_version: String,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for DeviceHeaders {
type Error = Errors<'static>;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = req.headers();
let get_h = |key: &str| headers.get_one(key).map(|s| s.to_string());
match (
get_h("x-nintendo-device-id"),
get_h("accept-language"),
get_h("x-nintendo-platform-id"),
get_h("x-nintendo-region"),
get_h("x-nintendo-serial-number"),
get_h("x-nintendo-system-version"),
) {
(
Some(device_id),
Some(accept_language),
Some(platform_id),
Some(region),
Some(serial_number),
Some(system_version),
) => Outcome::Success(DeviceHeaders {
device_id,
accept_language,
platform_id,
region,
serial_number,
system_version,
}),
_ => Outcome::Error((
rocket::http::Status::BadRequest,
Errors {
error: &[Error {
code: "1600",
message: "Unable to process request",
}],
},
)),
}
}
}
#[get("/v1/api/people/@me/devices")]
pub fn get_user_devices(
auth: Auth<false>,
headers: DeviceHeaders,
) -> Xml<DevicesWrapper> {
let now = chrono::Utc::now().naive_utc();
Xml(DevicesWrapper {
devices: vec![DeviceInfo {
device_id: headers.device_id,
language: headers.accept_language,
updated: now,
pid: auth.pid,
platform_id: headers.platform_id,
region: headers.region,
serial_number: headers.serial_number,
status: "ACTIVE".to_string(),
system_version: headers.system_version,
r#type: "RETAIL".to_string(),
updated_by: "USER".to_string(),
}],
})
}
#[get("/v1/api/people/@me/devices/status")]
pub fn get_device_status(_auth: Auth<false>) -> Xml<EmptyDeviceResponse> {
Xml(EmptyDeviceResponse {
device: String::new(),
})
}
#[put("/v1/api/people/@me/devices/@current/inactivate")]
pub fn inactivate_current_device(_auth: Auth<false>) -> () {
// just 200
}
#[post("/v1/api/people/@me/deletion")]
pub async fn delete_account(
database: &State<Pool>,
auth: Auth<false>,
) -> Result<(), Option<Errors<'static>>> {
let db = database.inner();
let result = sqlx::query!(
"DELETE FROM users WHERE pid = $1",
auth.pid
)
.execute(db)
.await;
if let Err(e) = result {
println!("failed to delete PID {}: {:?}", auth.pid, e);
return Err(Some(DATABASE_ERROR));
}
Ok(())
}
#[put("/v1/api/people/@me", data = "<data>")]
pub async fn update_account(
database: &State<Pool>,
auth: Auth<false>,
data: Xml<UpdateAccountData>,
) -> Result<(), Option<Errors<'static>>> {
let db = database.inner();
let pid = auth.pid;
let data = data.0;
let updated_password = if let Some(ref new_pass) = data.password {
generate_password(pid, new_pass)
} else {
None
};
let result = sqlx::query!(
"
UPDATE users SET
gender = COALESCE($1, gender),
region = COALESCE($2, region),
country = COALESCE($3, country),
language = COALESCE($4, language),
timezone = COALESCE($5, timezone),
marketing_allowed = COALESCE($6, marketing_allowed),
off_device_allowed = COALESCE($7, off_device_allowed),
password = COALESCE($8, password)
WHERE pid = $9
",
data.gender.as_deref(),
data.region,
data.country.as_deref(),
data.language.as_deref(),
data.tz_name.as_deref(),
data.marketing_flag.map(|v| v.0),
data.off_device_flag.map(|v| v.0),
updated_password,
pid
)
.execute(db)
.await;
if let Err(e) = result {
println!("failed to update account for PID {}: {:?}", pid, e);
return Err(Some(DATABASE_ERROR));
}
Ok(())
}
#[get("/v1/api/people/@me/emails")]
pub fn get_own_emails(user: Auth<false>) -> Xml<EmailsWrapper> {
let profile = build_profile(user.into());
Xml(EmailsWrapper {
emails: vec![profile.email],
})
}
#[put("/v1/api/people/@me/emails/@primary", data = "<data>")]
pub async fn update_primary_email(
database: &State<Pool>,
auth: Auth<false>,
data: Xml<UpdateEmailRequest>,
) -> Result<(), Option<Errors<'static>>> {
let db = database.inner();
let pid = auth.pid;
let new_address = data.0.email.address.to_lowercase();
let verification_code: i32 = rand::rng().random_range(100_000..1_000_000);
let result = sqlx::query!(
"
UPDATE users SET
email = $1,
email_verified_since = NULL,
verification_code = $2
WHERE pid = $3
",
new_address,
verification_code,
pid
)
.execute(db)
.await;
if let Err(e) = result {
println!("failed to update email for PID {}: {:?}", pid, e);
return Err(Some(DATABASE_ERROR));
}
if let Err(e) = send_verification_email(&new_address, verification_code, &auth.username).await {
println!("failed to send verification email: {e}");
}
Ok(())
}

View file

@ -6,6 +6,8 @@ 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 {
@ -14,6 +16,13 @@ const BAD_CODE_ERROR: Errors = Errors {
}],
};
const UNAUTHORIZED_DEVICE_ERROR: Errors = Errors {
error: &[Error {
code: "0113",
message: "Unauthorized device",
}],
};
#[derive(FromForm)]
pub struct ValidateEmailInput {
email: String,
@ -45,7 +54,6 @@ pub async fn validate(
let email = data.email.trim();
// 1. Validate presence + basic format
if email.is_empty() || !email.contains('@') {
return Err(Errors {
error: &[Error {
@ -55,7 +63,6 @@ pub async fn validate(
});
}
// 2. Extract domain safely
let domain = match email.split('@').nth(1) {
Some(d) if !d.is_empty() => d,
_ => {
@ -68,7 +75,7 @@ pub async fn validate(
}
};
// 3. DNS resolver
// 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 {
@ -77,7 +84,6 @@ pub async fn validate(
}],
})?;
// 4. MX lookup
match resolver.mx_lookup(domain).await {
Ok(mx) if mx.iter().next().is_some() => Ok(()),
@ -161,3 +167,64 @@ pub async fn resend_email(
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(())
}