parental controls
All checks were successful
Build and Test / account (push) Successful in 3m15s

This commit is contained in:
red binder 2026-08-11 03:34:00 +02:00
commit 124916e62c
6 changed files with 556 additions and 0 deletions

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO device_parental_controls (serial_number, email, updated_at)\n VALUES ($1, $2, NOW())\n ON CONFLICT (serial_number)\n DO UPDATE SET email = EXCLUDED.email, updated_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "7d5d57a9c0065301382767c85de4f25f0636ec0e7c0beed4cdb627c419e407c4"
}

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. You have requested a reset of parental controls on your console.</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.
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&nbsp;</td>
</tr>
<tr>
<td style="color: #ffffff; ">
Your email was linked to a Wii U console with serial number {{serial}} for parental controls.
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&nbsp;</td>
</tr>
<tr>
<td>
You have requested a master key to reset your parental controls PIN. The master key you must enter on your Wii U is:
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&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">
{{mkey}}
</td>
</tr>
<tr>
<td width="100%" height="36px" style="line-height: 36px;">&nbsp;</td>
</tr>
<tr>
<td>
Please make sure to change your PIN as soon as possible.
</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

@ -0,0 +1,177 @@
<!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. The console with the serial number {{serial}} has been linked to your email for parental controls.</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.
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&nbsp;</td>
</tr>
<tr>
<td style="color: #ffffff; ">
Your email was linked to a Wii U console with serial number {{serial}} for parental controls.
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&nbsp;</td>
</tr>
<tr>
<td>
If you did not authorize this, please contact our team at support@spbr.net.
</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

@ -70,3 +70,70 @@ pub async fn send_reset_email(to: &str, pwd: &str, username: &str) -> Result<(),
Ok(())
}
pub async fn send_pc_email(to: &str, serial: &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/parentalControlsTemplate.html")
.map_err(|e| format!("Failed to read email template: {}", e))?;
// Replace placeholders
let body = template
.replace("{{serial}}", serial);
let email = Message::builder()
.from(smtp_user.parse().unwrap())
.to(to.parse().unwrap())
.subject("Parental controls confirmation 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(())
}
pub async fn send_reset_pin_email(to: &str, serial: &str, master_key: &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/masterKeyTemplate.html")
.map_err(|e| format!("Failed to read email template: {}", e))?;
// Replace placeholders
let body = template
.replace("{{serial}}", serial)
.replace("{{mkey}}", master_key);
let email = Message::builder()
.from(smtp_user.parse().unwrap())
.to(to.parse().unwrap())
.subject("Parental controls PIN 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

@ -102,6 +102,8 @@ async fn launch() -> _ {
nnid::support::verify_email,
nnid::support::resend_email,
nnid::support::forgotten_password,
nnid::support::send_parental_controls_pin,
nnid::support::send_forgotten_pin,
nnid::people::create_account,
nnid::people::get_own_profile,
nnid::people::get_device_owner,

View file

@ -220,5 +220,110 @@ pub async fn forgotten_password(
UNAUTHORIZED_DEVICE_ERROR
})?;
Ok(())
}
pub struct DeviceSerial(pub String);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for DeviceSerial {
type Error = Errors<'static>;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
match req.headers().get_one("x-nintendo-serial-number") {
Some(val) if !val.trim().is_empty() => {
request::Outcome::Success(DeviceSerial(val.to_string()))
}
_ => request::Outcome::Error((
Status::BadRequest,
Errors {
error: &[Error {
code: "0130",
message: "Serial not found",
}],
},
)),
}
}
}
#[get("/v1/api/support/send_confirmation/pin/<email>")]
pub async fn send_parental_controls_pin(
database: &State<Pool>,
serial: DeviceSerial,
email: &str,
) -> Result<(), Errors<'static>> {
let db = database.inner();
let serial_number = serial.0;
let email = email.trim().to_lowercase();
if email.is_empty() || !email.contains('@') {
return Err(Errors {
error: &[Error {
code: "0103",
message: "Email format is invalid",
}],
});
}
let db_result = sqlx::query!(
"
INSERT INTO device_parental_controls (serial_number, email, updated_at)
VALUES ($1, $2, NOW())
ON CONFLICT (serial_number)
DO UPDATE SET email = EXCLUDED.email, updated_at = NOW()
",
serial_number,
email
)
.execute(db)
.await;
if let Err(e) = db_result {
eprintln!("Failed to associate serial {serial_number} with email {email}: {e}");
return Err(Errors {
error: &[Error {
code: "0130",
message: "PID has not been registered yet",
}],
});
}
crate::email::send_pc_email(&email, &serial_number)
.await
.map_err(|e| {
eprintln!("Failed to send parental controls email to {email}: {e}");
Errors {
error: &[Error {
code: "0130",
message: "PID has not been registered yet",
}],
}
})?;
Ok(())
}
#[get("/v1/api/support/send_forgotten/pin/<email>/<master_key>")]
pub async fn send_forgotten_pin(
serial: DeviceSerial,
email: &str,
master_key: &str,
) -> Result<(), Errors<'static>> {
let serial_number = serial.0;
let email = email.trim().to_lowercase();
crate::email::send_reset_pin_email(&email, &serial_number, &master_key)
.await
.map_err(|e| {
eprintln!("Failed to send parental controls email to {email}: {e}");
Errors {
error: &[Error {
code: "0130",
message: "PID has not been registered yet",
}],
}
})?;
Ok(())
}