Compare commits

..
37 changed files with 662 additions and 2048 deletions

View file

@ -1,16 +0,0 @@
{
"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

@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "select serial from certificates where hash = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "serial",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "certificates",
"name": "serial"
}
}
}
],
"parameters": {
"Left": [
"Bytea"
]
},
"nullable": [
true
]
},
"hash": "0ef5f08a84d6864291cb6125610d9d3e1961530ae963f9ab5442482d81228590"
}

View file

@ -0,0 +1,21 @@
{
"db_name": "PostgreSQL",
"query": "SELECT nextval('pid_counter') as pid",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pid",
"type_info": "Int8",
"origin": "Expression"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "3c9b1695f8ae49e4308c048de98c1c262351465b6f102e98912f67442a1f54d9"
}

View file

@ -68,17 +68,6 @@
"name": "expires"
}
}
},
{
"ordinal": 6,
"name": "cert_hash",
"type_info": "Bytea",
"origin": {
"Table": {
"table": "tokens",
"name": "cert_hash"
}
}
}
],
"parameters": {
@ -94,8 +83,7 @@
false,
false,
true,
false,
true
false
]
},
"hash": "48710e0b87742cc3fef816b3c95604095f71324011e7093ec37af15da8c158f4"

View file

@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT username, email FROM users WHERE pid = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "username",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "users",
"name": "username"
}
}
},
{
"ordinal": 1,
"name": "email",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "users",
"name": "email"
}
}
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
false,
false
]
},
"hash": "48fd98db66a68865babe4ac6a453de747bba2ce1da198dda4b6f3b78eacfebf3"
}

View file

@ -211,17 +211,6 @@
"name": "verification_code"
}
}
},
{
"ordinal": 19,
"name": "eula_version",
"type_info": "Int4",
"origin": {
"Table": {
"table": "users",
"name": "eula_version"
}
}
}
],
"parameters": {
@ -248,7 +237,6 @@
false,
false,
false,
false,
false
]
},

View file

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET password = $1 WHERE pid = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "6c276038b92bef06b30da32dab42256be431b361cfd36754aa41b6da220618cc"
}

View file

@ -1,15 +0,0 @@
{
"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

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET eula_version = $1 WHERE pid = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "7e74c28ce6daa8155e268756411f73f1d281a736aa5a838ad3e72fce0e9a109e"
}

View file

@ -1,22 +0,0 @@
{
"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

@ -211,17 +211,6 @@
"name": "verification_code"
}
}
},
{
"ordinal": 19,
"name": "eula_version",
"type_info": "Int4",
"origin": {
"Table": {
"table": "users",
"name": "eula_version"
}
}
}
],
"parameters": {
@ -248,7 +237,6 @@
false,
false,
false,
false,
false
]
},

View file

@ -68,17 +68,6 @@
"name": "expires"
}
}
},
{
"ordinal": 6,
"name": "cert_hash",
"type_info": "Bytea",
"origin": {
"Table": {
"table": "tokens",
"name": "cert_hash"
}
}
}
],
"parameters": {
@ -94,8 +83,7 @@
false,
false,
true,
false,
true
false
]
},
"hash": "9d3cee43a86cead9a6d078abc1266fc2a97ac6e25a9733d1d20faf555c67abe1"

View file

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "update certificates set serial = $1 where hash = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Bytea"
]
},
"nullable": []
},
"hash": "beeeea07993ba8daac1e43c985e8c840f66eec4e5458d3709c0e0f09afe51377"
}

View file

@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(select 1 from users where pid = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool",
"origin": "Expression"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
null
]
},
"hash": "c8d50662530cac49c4261fb321cd15f9e4bafdfca12d2130a873d44a88dd435b"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "insert into tokens (token_type, pid, title_id, cert_hash)\n values ($1, $2, $3, $4) returning token_id, random",
"query": "insert into tokens (token_type, pid, title_id)\n values ($1, $2, $3) returning token_id, random",
"describe": {
"columns": [
{
@ -30,8 +30,7 @@
"Left": [
"Int4",
"Int4",
"Varchar",
"Bytea"
"Varchar"
]
},
"nullable": [
@ -39,5 +38,5 @@
false
]
},
"hash": "d8298703381f0e220bdbe122dbdc4dc5cd7ae99fac34ae16023f9a6a701857c9"
"hash": "e5a2f7f28c3d7b9524d3dce48a9e47d6180ff634ebf59f3a1efd92b797170ac2"
}

View file

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

608
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,7 @@ chrono = { version = "0.4.39", features = ["serde"] }
argon2 = "0.5.3"
sha2 = "0.11.0"
bytemuck = { version = "1.21.0", features = ["derive"] }
base64 = "0.23.0"
base64 = "0.22.1"
hex = "0.4.3"
thiserror = "2.0.11"
bcrypt = "0.19.0"
@ -36,7 +36,7 @@ md-5 = "0.11.0"
cbc = "0.1.2"
crc32fast = "1.4.2"
gxhash = "3.4.1"
sentry = "0.49.0"
sentry = "0.48.0"
rocket_cors = "0.6.0"
juniper = { version = "0.17.0", features = ["chrono"] }
@ -47,11 +47,11 @@ lettre = "0.11.15"
rand = { version = "0.10.0", features = ["std"] }
reqwest = "0.13.0"
binrw = "0.15.1"
ecdsa = { version = "0.17.0", features = ["pem", "std", "algorithm", "digest"] }
ecdsa = { version = "0.16.9", features = ["pem", "std", "verifying"] }
sha256 = "1.6.0"
p256 = "0.14.0"
k256 = "0.14.0"
dsa = "0.7.0"
k256 = "0.13.4"
dsa = "0.6.3"
openssl = {version = "0.10.78", features = ["vendored"]}
time = "0.3.47"
hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }

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: 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>
<?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>

0
res/agreement/EVIL.xml Normal file
View file

View file

@ -38,7 +38,7 @@
color: #B60000 !important;
}
img.logo {
content: url("https://spfn.spbr.net/res/img/spfn.png") !important;
content: url("https://spfn.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.spbr.net">
<img class="logo" width="auto" height="48px" src="https://spfn.spbr.net/res/img/spfn.png" alt="SPFN">
<a href="https://spfn.net">
<img class="logo" width="auto" height="48px" src="https://spfn.net/res/img/spfn.png" alt="SPFN">
</a>
</td>
</tr>
@ -122,6 +122,19 @@
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

@ -1,190 +0,0 @@
<!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

@ -1,177 +0,0 @@
<!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

@ -1,190 +0,0 @@
<!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

@ -64,7 +64,6 @@ pub struct User {
pub updated: NaiveDateTime,
pub nex_password: String,
pub verification_code: Option<i32>,
pub eula_version: i32,
}
#[derive(sqlx::FromRow)]
@ -118,10 +117,7 @@ pub fn generate_password(pid: i32, cleartext_password: &str) -> Option<String> {
bcrypt::hash(password, 10).ok()
}
pub async fn read_basic_auth_token(
connection: &Pool,
token: &str,
) -> Option<(User, Option<[u8; 32]>)> {
pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option<User> {
let data = match BASE64_STANDARD.decode(&token) {
Ok(d) => d,
Err(e) => {
@ -165,69 +161,13 @@ pub async fn read_basic_auth_token(
let password_valid = user.verify_cleartext_password(&login_password);
if password_valid == Some(true) {
Some((user, None))
Some(user)
} else {
None
}
}
pub async fn read_hashed_basic_auth_token(
connection: &Pool,
token: &str,
) -> Option<(User, Option<[u8; 32]>)> {
let data = match BASE64_STANDARD.decode(token) {
Ok(d) => d,
Err(e) => {
println!("Failed to decode base64: {:?}", e);
return None;
}
};
let decoded_token = match String::from_utf8(data) {
Ok(s) => s,
Err(e) => {
println!("Failed to convert decoded bytes to UTF-8 string: {:?}", e);
return None;
}
};
let (login_username, login_hash) = match decoded_token.split_once(' ') {
Some(parts) => parts,
None => {
println!("Failed to split hashed basic token into username and hash");
return None;
}
};
let user_result = sqlx::query_as!(
User,
"SELECT * FROM users WHERE username = $1",
login_username
)
.fetch_one(connection)
.await;
let user = match user_result {
Ok(u) => u,
Err(e) => {
println!("Failed to fetch user from database: {:?}", e);
return None;
}
};
let password_valid = user.verify_hashed_password(login_hash);
if password_valid == Some(true) {
Some((user, None))
} else {
None
}
}
pub async fn read_bearer_auth_token(
connection: &Pool,
token: &str,
) -> Option<(User, Option<[u8; 32]>)> {
pub async fn read_bearer_auth_token(connection: &Pool, token: &str) -> Option<User> {
let data = TokenData::decode(token)?;
let token_info = sqlx::query!(
@ -249,11 +189,11 @@ pub async fn read_bearer_auth_token(
.await
.ok()?;
Some((user, token_info.cert_hash.and_then(|v| v.try_into().ok())))
Some(user)
}
pub fn generate_nex_password() -> String {
let mut rng = rand::rng();
let mut rng = rand::rng();
let mut output = String::with_capacity(16);
while output.len() < 16 {
@ -274,7 +214,6 @@ pub fn generate_nex_password() -> String {
pub struct Auth<const FORCE_BEARER_AUTH: bool = true, const USE_CERT: bool = FORCE_BEARER_AUTH>(
pub User,
pub Option<[u8; 32]>,
);
impl<const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> AsRef<User>
@ -339,7 +278,6 @@ pub async fn link_certificate_to_pid(
Ok(())
}
// todo: make this more consistent by requiring a cert to be asociated to the token if USE_CERT is set
#[async_trait]
impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
for Auth<FORCE_BEARER_AUTH, USE_CERT>
@ -359,14 +297,13 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
let (auth_type, token) = request_try!(auth.split_once(' ').ok_or(INVALID_TOKEN_ERRORS));
let data = match auth_type {
let user = match auth_type {
"Basic" if !FORCE_BEARER_AUTH => read_basic_auth_token(pool, token).await,
"HashedBasic" if !FORCE_BEARER_AUTH => read_hashed_basic_auth_token(pool, token).await,
"Bearer" => read_bearer_auth_token(pool, token).await,
_ => return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS)),
};
let Some((user, cert)) = data else {
let Some(user) = user else {
return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS));
};
@ -379,7 +316,7 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
// ..user
// };
Outcome::Success(Self(user, cert))
Outcome::Success(Self(user))
}
}

View file

@ -36,104 +36,3 @@ 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(())
}
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

@ -1,7 +1,11 @@
use rocket::serde::json::Json;
use rocket::{post, FromForm, State};
use rocket::form::Form;
use rocket::futures::TryFutureExt;
use rocket::http::Status;
use crate::account::account::Auth;
use crate::json_api::oauth::generate_token::TokenRequest;
use crate::nnid::people::{build_oauth_profile, GetOwnOAuthProfileData};
use crate::Pool;
#[derive(FromForm)]
@ -17,7 +21,7 @@ pub async fn ban_user(pool: &State<Pool>, auth: Auth<true>, request: Form<AdminR
log::info!("banning user {:?} from moderator {:?}", request.username, auth.username);
let _row = sqlx::query!(
let row = sqlx::query!(
"UPDATE users SET account_level = $1 WHERE username = $2",
-1,
request.username

View file

@ -65,13 +65,12 @@ pub async fn authorize_submit(
let expires_at = Utc::now().naive_utc() + Duration::minutes(5);
if let Err(db_err) = sqlx::query(
"INSERT INTO oauth_auth_codes (code, pid, redirect_uri, expires_at, client_id) VALUES ($1, $2, $3, $4, $5)"
"INSERT INTO oauth_auth_codes (code, pid, redirect_uri, expires_at) VALUES ($1, $2, $3, $4)"
)
.bind(&secure_auth_code)
.bind(pid)
.bind(form_data.redirect_uri)
.bind(expires_at)
.bind(form_data.client_id)
.execute(pool.inner())
.await {
eprintln!("failed to save auth code: {:?}", db_err);

View file

@ -1,13 +1,19 @@
use crate::Pool;
use serde::Serialize;
use rocket::FromForm;
use sha2::{Sha256, Digest};
use bytemuck::bytes_of;
use rocket::{post, State, form::Form, http::Status, serde::json::Json};
use sqlx::Row;
use std::env;
use once_cell::sync::Lazy;
use crate::nnid::oauth::generate_token::create_token;
use crate::nnid::oauth::generate_token::token_type::AUTH_TOKEN;
use bytemuck::bytes_of;
use chrono::Utc;
use rocket::FromForm;
use rocket::{State, form::Form, http::Status, post, serde::json::Json};
use serde::Serialize;
use sha2::{Digest, Sha256};
use sqlx::Row;
pub static CLIENT_SECRET: Lazy<String> = Lazy::new(|| {
env::var("OAUTH_CLIENT_SECRET").expect("OAUTH_CLIENT_SECRET not set")
});
#[derive(Serialize)]
pub struct OAuthTokenResponse {
@ -28,14 +34,17 @@ pub struct TokenRequest<'r> {
}
pub fn verify_nintendo_password(pid: i32, text_password: &str, db_bcrypt_hash: &str) -> bool {
// maple: binder, there's already a function for this, why duplicate code?
// binder: dear maple, with this function i can just hand it a pid, the password from the user and the db_bcrypt_hash and it gives me a true/false value.
let mut sha = Sha256::new();
sha.update(bytes_of(&pid));
sha.update(&[0x02, 0x65, 0x43, 0x46]);
sha.update(text_password.as_bytes());
let hashed_password_hex = hex::encode(sha.finalize());
bcrypt::verify(hashed_password_hex, db_bcrypt_hash).unwrap_or(false)
bcrypt::verify(hashed_password_hex, db_bcrypt_hash).unwrap_or_else(|_| false)
}
// dummy error responses
#[derive(Serialize)]
pub struct OAuthErrorResponse {
pub error: String,
@ -43,92 +52,53 @@ pub struct OAuthErrorResponse {
#[post("/api/v2/oauth2/generate_token", data = "<form_data>")]
pub async fn generate_token(
pool: &State<Pool>,
form_data: Form<TokenRequest<'_>>,
pool: &State<Pool>,
form_data: Form<TokenRequest<'_>>
) -> Result<Json<OAuthTokenResponse>, (Status, Json<OAuthErrorResponse>)> {
println!("redirect URI is: {:?}", form_data.redirect_uri);
let client_id = form_data.client_id.ok_or((
Status::BadRequest,
Json(OAuthErrorResponse {
error: "invalid_client".to_string(),
}),
))?;
let client_row = match sqlx::query(
"SELECT client_secret, is_confidential FROM oauth_clients WHERE client_id = $1",
)
.bind(client_id)
.fetch_optional(pool.inner())
.await
{
Ok(Some(row)) => row,
match form_data.client_id {
Some("account") | Some("splatnet") => {
let cl_secret: String = CLIENT_SECRET.clone();
if form_data.client_secret != Some(&cl_secret) {
return Err((
Status::Unauthorized,
Json(OAuthErrorResponse { error: "invalid_client".to_string() })
));
}
}
Some("website") => {
// no secret for this client, cant be kept confidential in the case of the website
}
_ => {
return Err((
Status::Unauthorized,
Json(OAuthErrorResponse {
error: "invalid_client".to_string(),
}),
));
}
};
let db_client_secret: Option<String> = client_row.get("client_secret");
let is_confidential: bool = client_row.get("is_confidential");
if is_confidential {
let provided_secret = form_data.client_secret.ok_or((
Status::Unauthorized,
Json(OAuthErrorResponse {
error: "invalid_client".to_string(),
}),
))?;
let matches = match db_client_secret {
Some(secret) => secret == provided_secret,
None => false,
};
if !matches {
return Err((
Status::Unauthorized,
Json(OAuthErrorResponse {
error: "invalid_client".to_string(),
}),
Status::BadRequest,
Json(OAuthErrorResponse { error: "invalid_client".to_string() })
));
}
}
// i'm only supporting the password grant for the id server.
// i'm only supporting the password grant incase someone feels lazy.
let pid: i32 = match form_data.grant_type {
"password" => {
let username = form_data.username.ok_or((
Status::BadRequest,
Json(OAuthErrorResponse {
error: "invalid_request".to_string(),
}),
Json(OAuthErrorResponse { error: "invalid_request".to_string() })
))?;
let password = form_data.password.ok_or((
Status::BadRequest,
Json(OAuthErrorResponse {
error: "invalid_request".to_string(),
}),
Json(OAuthErrorResponse { error: "invalid_request".to_string() })
))?;
let user_row = match sqlx::query("SELECT pid, password FROM users WHERE username = $1")
.bind(username)
.fetch_optional(pool.inner())
.await
.await
{
Ok(Some(row)) => row,
_ => {
return Err((
Status::BadRequest,
Json(OAuthErrorResponse {
error: "invalid_grant".to_string(),
}),
));
}
_ => return Err((
Status::BadRequest,
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
)),
};
let db_pid: i32 = user_row.get("pid");
@ -137,39 +107,31 @@ pub async fn generate_token(
if !verify_nintendo_password(db_pid, password, &db_bcrypt_hash) {
return Err((
Status::Unauthorized,
Json(OAuthErrorResponse {
error: "invalid_grant".to_string(),
}),
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
));
}
db_pid
}
},
"authorization_code" => {
let incoming_code = form_data.code.ok_or((
Status::BadRequest,
Json(OAuthErrorResponse {
error: "invalid_request".to_string(),
}),
Status::BadRequest,
Json(OAuthErrorResponse { error: "invalid_request".to_string() })
))?;
let code_row = match sqlx::query(
"SELECT pid, expires_at, used FROM oauth_auth_codes WHERE code = $1",
"SELECT pid, expires_at, used FROM oauth_auth_codes WHERE code = $1"
)
.bind(incoming_code)
.fetch_optional(pool.inner())
.await
{
Ok(Some(row)) => row,
_ => {
return Err((
Status::BadRequest,
Json(OAuthErrorResponse {
error: "invalid_grant".to_string(),
}),
));
}
_ => return Err((
Status::BadRequest,
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
)),
};
let is_used: bool = code_row.get("used");
@ -178,10 +140,8 @@ pub async fn generate_token(
if is_used || expires_at < Utc::now().naive_utc() {
return Err((
Status::BadRequest,
Json(OAuthErrorResponse {
error: "invalid_grant".to_string(),
}),
Status::BadRequest,
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
));
}
@ -191,23 +151,19 @@ pub async fn generate_token(
.await;
target_pid
}
},
_ => {
return Err((
Status::BadRequest,
Json(OAuthErrorResponse {
error: "unsupported_grant_type".to_string(),
}),
));
}
_ => return Err((
Status::BadRequest,
Json(OAuthErrorResponse { error: "unsupported_grant_type".to_string() })
))
};
let token = create_token(pool.inner(), pid, AUTH_TOKEN, None, None).await;
let token = create_token(pool.inner(), pid, AUTH_TOKEN, None).await;
Ok(Json(OAuthTokenResponse {
access_token: token,
token_type: "Bearer".to_string(),
expires_in: 3600,
}))
}
}

View file

@ -1,40 +0,0 @@
use rocket::serde::json::Json;
use rocket::{get, State};
use rocket::http::Status;
use crate::account::account::Auth;
use serde::Serialize;
use crate::Pool;
#[derive(Serialize)]
pub struct ConsoleData {
pub serial: String,
pub cert_hash: String,
}
#[get("/api/v2/users/@me/console")]
pub async fn get_own_console(pool: &State<Pool>, auth: Auth<true>) -> Result<Json<ConsoleData>, (Status, &'static str)> {
let hash = auth.1.ok_or((Status::BadRequest, "token needs cert hash"))?;
let row = sqlx::query!(
"select serial from certificates where hash = $1",
&hash[..]
)
.fetch_one(pool.inner())
.await
.map_err(|e| {
log::error!("failed to execute query: {:?}", e);
(Status::InternalServerError, "error in database query")
})?;
let hex_hash = hex::encode(hash);
let serial = row.serial.ok_or((Status::BadRequest, "we had no serial"))?;
Ok(
Json (
ConsoleData {
serial,
cert_hash: hex_hash
}
)
)
}

View file

@ -1,4 +1,3 @@
pub mod profile;
pub mod mii;
pub mod delete;
pub mod console;
pub mod delete;

View file

@ -101,21 +101,11 @@ async fn launch() -> _ {
nnid::support::validate,
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,
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,
@ -129,7 +119,6 @@ async fn launch() -> _ {
json_api::oauth::authorize::authorize_page,
json_api::oauth::authorize::authorize_submit,
json_api::admin::bans::ban_user,
json_api::users::console::get_own_console,
nnid::people::thing,
// graphql::graphiql,
// graphql::playground,

View file

@ -1,64 +1,37 @@
use std::{env, io, fs};
use serde::Deserialize;
use std::{env, io};
use std::collections::HashSet;
use once_cell::sync::Lazy;
use rocket::fs::NamedFile;
use rocket::get;
use rocket::{get, Request};
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use rocket::response::content::RawXml;
use tokio::fs::try_exists;
use tokio::sync::RwLock;
use rocket::async_trait;
use crate::dsresponse::Ds;
// pub static EVIL_AGREEMENT_THING: Lazy<RwLock<HashSet<String>>> = Lazy::new(|| Default::default());
pub static EVIL_AGREEMENT_THING: Lazy<RwLock<HashSet<String>>> = Lazy::new(|| Default::default());
// pub struct CFIP(pub String);
pub struct CFIP(pub String);
// #[async_trait]
// impl<'r> FromRequest<'r> for CFIP{
// type Error = ();
#[async_trait]
impl<'r> FromRequest<'r> for CFIP{
type Error = ();
// async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
// match request.headers().get("CF-Connecting-IP").next(){
// Some(v) => Outcome::Success(Self(v.to_owned())),
// None => Outcome::Error((Status::ImATeapot, ()))
// }
// }
// }
#[derive(Debug, Deserialize)]
struct Agreement {
version: String,
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.headers().get("CF-Connecting-IP").next(){
Some(v) => Outcome::Success(Self(v.to_owned())),
None => Outcome::Error((Status::ImATeapot, ()))
}
}
}
#[derive(Debug, Deserialize)]
struct Agreements {
#[serde(rename = "agreement")]
agreements: Vec<Agreement>,
}
pub fn get_latest_eula_version() -> Result<String, Box<dyn std::error::Error>> {
let mut path = std::env::current_dir()?;
path.push("res");
path.push("agreement");
path.push("DEFAULT.xml");
let content = fs::read_to_string(path)?;
let parsed: Agreements = quick_xml::de::from_str(&content)?;
parsed
.agreements
.first()
.map(|a| a.version.clone())
.ok_or_else(|| "No agreement found in XML".into())
}
pub static LATEST_EULA_VERSION: Lazy<i32> = Lazy::new(|| {
get_latest_eula_version()
.ok()
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(300)
});
#[get("/v1/api/content/agreements/Nintendo-Network-EULA/<lang>/@latest")]
pub async fn get_agreement(lang: &str) -> io::Result<Ds<RawXml<NamedFile>>>{
pub async fn get_agreement(lang: &str, ip: CFIP) -> io::Result<Ds<RawXml<NamedFile>>>{
let base_path = {
// if this crashes then something is wrong with the server setup so crashing here is fine imo
let mut path = env::current_dir().unwrap();
@ -69,26 +42,38 @@ pub async fn get_agreement(lang: &str) -> io::Result<Ds<RawXml<NamedFile>>>{
path
};
let requested_file_path = {
let mut path = base_path.clone();
path.push(format!("{}.xml", lang));
path
};
if try_exists(&requested_file_path).await.is_ok_and(|v| v == true) {
Ok(Ds(RawXml(NamedFile::open(&requested_file_path).await?)))
} else {
let fallback_path = {
if EVIL_AGREEMENT_THING.read().await.contains(&ip.0) {
let path = {
let mut path = base_path;
path.push("DEFAULT.xml");
path.push("EVIL.xml");
path
};
Ok(Ds(RawXml(NamedFile::open(&fallback_path).await?)))
Ok(Ds(RawXml(NamedFile::open(&path).await?)))
} else {
let requested_file_path = {
let mut path = base_path.clone();
path.push(format!("{}.xml", lang));
path
};
if try_exists(&requested_file_path).await.is_ok_and(|v| v == true) {
Ok(Ds(RawXml(NamedFile::open(&requested_file_path).await?)))
} else {
let fallback_path = {
let mut path = base_path;
path.push("DEFAULT.xml");
path
};
Ok(Ds(RawXml(NamedFile::open(&fallback_path).await?)))
}
}
}

View file

@ -1,50 +1,50 @@
#![allow(unused)]
use crate::Pool;
use rocket::{post, FromForm, State};
use rocket::form::Form;
use serde::{Serialize};
use crate::account::account::{Auth, DeviceCert, User, link_certificate_to_pid};
use crate::error::{Error, Errors};
use crate::nnid::oauth::TokenData;
use crate::nnid::agreements::{CFIP, EVIL_AGREEMENT_THING};
use crate::nnid::oauth::generate_token::token_type::{AUTH_REFRESH_TOKEN, AUTH_TOKEN};
use crate::nnid::agreements::LATEST_EULA_VERSION;
use crate::nnid::oauth::TokenData;
use crate::Pool;
use crate::xml::Xml;
use rocket::form::Form;
use rocket::{FromForm, State, post};
use serde::Serialize;
pub mod token_type {
pub mod token_type{
pub const AUTH_REFRESH_TOKEN: i32 = 1;
pub const AUTH_TOKEN: i32 = 0;
pub const NEX_TOKEN: i32 = 2;
}
const ACCOUNT_ID_OR_PASSWORD_ERRORS: Errors = Errors {
error: &[Error {
code: "0106",
message: "Invalid account ID or password",
}],
const ACCOUNT_ID_OR_PASSWORD_ERRORS: Errors = Errors{
error: &[
Error{
code: "0106",
message: "Invalid account ID or password"
}
]
};
const ACCOUNT_BANNED_ERRORS: Errors = Errors {
error: &[Error {
code: "0108",
message: "Account banned from server",
}],
const ACCOUNT_BANNED_ERRORS: Errors = Errors{
error: &[
Error{
code: "0108",
message: "Account banned from server"
}
]
};
const ACCOUNT_TEMPBANNED_ERRORS: Errors = Errors {
error: &[Error {
code: "0132",
message: "Account temporarily banned from server",
}],
};
const REREAD_EULA_ERRORS: Errors = Errors {
error: &[Error {
code: "0109",
message: "The EULA has been updated",
}],
const REREAD_EULA_EXTRABANNED_ERRORS: Errors = Errors{
error: &[
Error{
code: "0109",
message: "REREAD THE EULA LOL"
}
]
};
#[derive(FromForm)]
pub struct TokenRequestData<'a> {
pub struct TokenRequestData<'a>{
grant_type: &'a str,
user_id: &'a str,
password: &'a str,
@ -55,27 +55,17 @@ pub struct TokenRequestData<'a> {
pub struct TokenReturnData {
token: String,
refresh_token: String,
expires_in: i32,
expires_in: i32
}
pub async fn create_token(
pool: &Pool,
pid: i32,
token_type: i32,
title_id: Option<&str>,
cert_hash: Option<&[u8]>,
) -> String {
pub async fn create_token(pool: &Pool, pid: i32, token_type: i32, title_id: Option<&str>) -> String{
let data = sqlx::query!(
"insert into tokens (token_type, pid, title_id, cert_hash)
values ($1, $2, $3, $4) returning token_id, random",
token_type,
pid,
title_id,
cert_hash
)
.fetch_one(pool)
.await
.unwrap();
"insert into tokens (token_type, pid, title_id)
values ($1, $2, $3) returning token_id, random",
token_type, pid, title_id
)
.fetch_one(pool)
.await.unwrap();
let token_id = data.token_id;
let random = data.random;
@ -83,77 +73,61 @@ pub async fn create_token(
let token = TokenData {
token_id,
random,
pid,
pid
};
token.encode().to_string()
}
impl TokenReturnData {
async fn new(pid: i32, pool: &Pool) -> Self {
let token = create_token(pool, pid, AUTH_TOKEN, None, None).await;
async fn new(pid: i32, pool: &Pool) -> Self{
let token = create_token(pool, pid, AUTH_TOKEN, None).await;
let refresh_token = create_token(pool, pid, AUTH_REFRESH_TOKEN, None, None).await;
let refresh_token = create_token(pool, pid, AUTH_REFRESH_TOKEN, None).await;
Self {
Self{
token,
refresh_token,
expires_in: 3600,
}
}
async fn new_with_cert(pid: i32, cert: Option<&[u8]>, pool: &Pool) -> Self {
let token = create_token(pool, pid, AUTH_TOKEN, None, cert).await;
let refresh_token = create_token(pool, pid, AUTH_REFRESH_TOKEN, None, cert).await;
Self {
token,
refresh_token,
expires_in: 3600,
expires_in: 3600
}
}
}
#[derive(Serialize)]
#[serde(rename = "OAuth20")]
pub struct TokenRequestReturnData {
access_token: TokenReturnData,
#[serde(rename="OAuth20")]
pub struct TokenRequestReturnData{
access_token: TokenReturnData
}
#[post("/v1/api/oauth20/access_token/generate", data = "<data>")]
pub async fn generate_token(
pool: &State<Pool>,
data: Form<TokenRequestData<'_>>,
cert: DeviceCert,
) -> Result<Xml<TokenRequestReturnData>, Option<Errors<'static>>> {
#[post("/v1/api/oauth20/access_token/generate", data="<data>")]
pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>, ip: CFIP, cert: DeviceCert) -> Result<Xml<TokenRequestReturnData>, Option<Errors<'static>>>{
let pool = pool.inner();
let user = User::get_by_username(data.user_id, pool)
.await
let user = User::get_by_username(data.user_id, pool).await
.ok_or(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS))?;
if !user
.verify_hashed_password(&data.password)
.is_some_and(|v| v)
{
if !user.verify_hashed_password(&data.password).is_some_and(|v| v){
return Err(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS));
}
if user.eula_version != *LATEST_EULA_VERSION {
return Err(Some(REREAD_EULA_ERRORS));
}
if user.account_level < 0 {
if user.account_level < 0{
if user.account_level == -2 {
return Err(Some(ACCOUNT_TEMPBANNED_ERRORS));
return Err(Some(REREAD_EULA_EXTRABANNED_ERRORS));
}
if user.account_level == -3{
EVIL_AGREEMENT_THING.write().await.insert(ip.0);
return Err(Some(REREAD_EULA_EXTRABANNED_ERRORS));
}
return Err(Some(ACCOUNT_BANNED_ERRORS));
}
link_certificate_to_pid(&pool, &cert.0, user.pid).await?;
let access_token =
TokenReturnData::new_with_cert(user.pid, Some(&cert.0.hash()[..]), pool).await;
let access_token = TokenReturnData::new(user.pid, pool).await;
Ok(Xml(TokenRequestReturnData { access_token }))
}
Ok(Xml(TokenRequestReturnData{
access_token
}))
}

View file

@ -13,12 +13,11 @@ 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 {
error: &[Error {
code: "2001",
code: "9999",
message: "Internal server error",
}],
};
@ -65,67 +64,6 @@ 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>,
@ -236,17 +174,17 @@ pub async fn create_account(
// }
#[derive(Serialize)]
pub struct EmailInfoOwnProfileData {
pub address: String,
pub id: u32,
pub parent: YesNoVal,
pub primary: YesNoVal,
pub reachable: YesNoVal,
struct EmailInfoOwnProfileData {
address: String,
id: u32,
parent: YesNoVal,
primary: YesNoVal,
reachable: YesNoVal,
#[serde(rename = "type")]
pub email_type: String,
pub updated_by: String,
pub validated: YesNoVal,
pub validated_date: Option<NaiveDateTime>,
email_type: String,
updated_by: String,
validated: YesNoVal,
validated_date: Option<NaiveDateTime>,
}
#[derive(Serialize)]
@ -556,245 +494,5 @@ pub async fn change_mii(
Ok(())
}
#[derive(Deserialize, Debug)]
pub struct AgreedEulaData {
pub agreement_date: NaiveDateTime,
pub country: Box<str>,
pub location: Box<str>,
pub version: i32
}
#[post("/v1/api/people/@me/agreements", data = "<data>")]
pub async fn thing(
database: &State<Pool>,
auth: Auth<false>,
data: Xml<AgreedEulaData>,
) -> Result<(), Option<Errors<'static>>> {
let db = database.inner();
let version = data.version;
let pid = auth.pid;
println!("eula data: {:?}", data.version);
let result = sqlx::query!(
"UPDATE users SET eula_version = $1 WHERE pid = $2",
version,
pid
)
.execute(db)
.await;
if let Err(e) = result {
println!("Failed to update EULA version for PID {}: {:?}", pid, e);
return Err(Some(DATABASE_ERROR));
}
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<true>,
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<true>) -> Xml<EmptyDeviceResponse> {
Xml(EmptyDeviceResponse {
device: String::new(),
})
}
#[put("/v1/api/people/@me/devices/@current/inactivate")]
pub fn inactivate_current_device(_auth: Auth<true>) -> () {
// just 200
}
#[post("/v1/api/people/@me/deletion")]
pub async fn delete_account(
database: &State<Pool>,
auth: Auth<true>,
) -> 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<true>,
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<true>,
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(())
}
#[post("/v1/api/people/@me/agreements")]
pub async fn thing() {}

View file

@ -4,13 +4,10 @@ use crate::error::{Error, Errors};
use crate::nnid::oauth::generate_token::create_token;
use crate::nnid::oauth::generate_token::token_type::NEX_TOKEN;
use crate::xml::Xml;
use log::{info, warn};
use nex_account::grpc::Pid;
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome, Request};
use rocket::{State, async_trait, get};
use reqwest::header::SERVER;
use rocket::{State, get};
use serde::Serialize;
use sqlx::query;
use sqlx::types::ipnetwork::IpNetwork::V4;
use std::net::Ipv4Addr;
@ -58,54 +55,10 @@ pub struct ServiceToken {
token: String,
}
pub async fn store_or_check_serial(pool: &Pool, serial: &str, cert_hash: [u8; 32]) -> bool {
let Ok(res) = query!(
"select serial from certificates where hash = $1",
&cert_hash[..]
)
.fetch_one(pool)
.await
else {
warn!(
"user tried to access a route which is locked behind a console asociated token without a console token"
);
return false;
};
let Some(stored_serial) = res.serial else {
let _row = query!(
"update certificates set serial = $1 where hash = $2",
serial,
&cert_hash[..]
).execute(pool)
.await
.ok();
return true;
};
serial == stored_serial
}
pub struct Serial(pub String);
#[async_trait]
impl<'r> FromRequest<'r> for Serial {
type Error = Errors<'static>;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let Some(header) = request.headers().get("X-Nintendo-Serial-Number").next() else {
warn!("serial number wasnt sent on request which expected a serial number");
return Outcome::Error((Status::BadRequest, SERVER_ERROR));
};
Outcome::Success(Self(header.to_owned()))
}
}
#[get("/v1/api/provider/service_token/@me")]
pub async fn get_service_token(
pool: &State<Pool>,
auth: Auth<true, false>,
serial: Serial,
) -> Result<Xml<ServiceToken>, Option<Errors<'static>>> {
// just gonna put this here as a side note for the future:
// we could also be using key derivation to derive the nex token as if it were a key
@ -116,17 +69,7 @@ pub async fn get_service_token(
let pool = pool.inner();
let Some(cert_hash) = auth.1 else {
info!("attempt to generate service token using non wii u/certificate token");
return Err(Some(SERVER_ERROR));
};
if !store_or_check_serial(pool, &serial.0, cert_hash).await {
info!("serial mismatched with certificate");
return Err(Some(SERVER_ERROR));
}
let token = create_token(pool, auth.pid, NEX_TOKEN, None, Some(&cert_hash[..])).await;
let token = create_token(pool, auth.pid, NEX_TOKEN, None).await;
Ok(Xml(ServiceToken { token }))
}

View file

@ -6,8 +6,6 @@ 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 {
@ -16,13 +14,6 @@ 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,
@ -54,6 +45,7 @@ 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 {
@ -63,6 +55,7 @@ pub async fn validate(
});
}
// 2. Extract domain safely
let domain = match email.split('@').nth(1) {
Some(d) if !d.is_empty() => d,
_ => {
@ -75,7 +68,7 @@ pub async fn validate(
}
};
// This shouldn't ever fail unless there's something wrong with the server
// 3. DNS resolver
let resolver = TokioAsyncResolver::tokio_from_system_conf()
.map_err(|_| Errors {
error: &[Error {
@ -84,6 +77,7 @@ pub async fn validate(
}],
})?;
// 4. MX lookup
match resolver.mx_lookup(domain).await {
Ok(mx) if mx.iter().next().is_some() => Ok(()),
@ -165,165 +159,5 @@ pub async fn resend_email(
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 update_result = sqlx::query!(
"UPDATE users SET password = $1 WHERE pid = $2",
nintendo_hash,
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(())
}
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(())
}