switch to using nex-account for nex account management
All checks were successful
Build and Test / account (push) Successful in 23m46s

This commit is contained in:
Maple Nebel 2026-07-05 22:13:51 +02:00
commit 2c7767124e
6 changed files with 1364 additions and 1066 deletions

View file

@ -1,2 +1,5 @@
[target.'cfg(target_arch = "x86_64")'] [target.'cfg(target_arch = "x86_64")']
rustflags = ["-C", "target-feature=+aes,+sse2"] rustflags = ["-C", "target-feature=+aes,+sse2"]
[registries]
spbr = { index = "sparse+https://crates.spbr.net/api/v1/crates/" }

1992
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -55,3 +55,4 @@ dsa = "0.6.3"
openssl = {version = "0.10.78", features = ["vendored"]} openssl = {version = "0.10.78", features = ["vendored"]}
time = "0.3.47" time = "0.3.47"
hickory-resolver = { version = "0.24", features = ["tokio-runtime"] } hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }
nex-account = { version = "0.2.3", registry = "spbr" }

View file

@ -1,31 +1,30 @@
#![allow(unused)] #![allow(unused)]
use chrono::{NaiveDate, NaiveDateTime}; use crate::Pool;
use gxhash::{gxhash32, gxhash64};
use rocket::{get, post, put, State};
use rocket::serde::{Deserialize, Serialize};
use crate::account::account::{Auth, User, generate_nex_password, generate_password}; use crate::account::account::{Auth, User, generate_nex_password, generate_password};
use crate::dsresponse::Ds; use crate::dsresponse::Ds;
use crate::error::{Error, Errors};
use crate::nnid::pid_distribution::next_pid;
use crate::nnid::timezones::{OFFSET_FROM_TIMEZONE};
use crate::Pool;
use crate::xml::{Xml, YesNoVal};
use crate::email::send_verification_email; use crate::email::send_verification_email;
use rand::prelude::*; use crate::error::{Error, Errors};
use crate::mii_util::get_mii_img_url; use crate::mii_util::get_mii_img_url;
use crate::nnid::timezones::OFFSET_FROM_TIMEZONE;
use crate::xml::{Xml, YesNoVal};
use chrono::{NaiveDate, NaiveDateTime};
use gxhash::{gxhash32, gxhash64};
use nex_account::grpc::{ActStageInfo, ActStageReturn};
use nex_account::grpc_client;
use rand::prelude::*;
use rocket::serde::{Deserialize, Serialize};
use rocket::{State, get, post, put};
const DATABASE_ERROR: Errors = Errors{ const DATABASE_ERROR: Errors = Errors {
error: &[ error: &[Error {
Error{
code: "9999", code: "9999",
message: "Internal server error" message: "Internal server error",
} }],
]
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Email{ pub struct Email {
address: Box<str> address: Box<str>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -36,7 +35,7 @@ pub struct UpdateMiiData {
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct Mii{ pub struct Mii {
name: Box<str>, name: Box<str>,
primary: YesNoVal, primary: YesNoVal,
data: Box<str>, data: Box<str>,
@ -44,7 +43,7 @@ pub struct Mii{
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename(serialize = "person"))] #[serde(rename(serialize = "person"))]
pub struct AccountCreationData{ pub struct AccountCreationData {
birth_date: NaiveDate, birth_date: NaiveDate,
user_id: Box<str>, user_id: Box<str>,
password: Box<str>, password: Box<str>,
@ -56,24 +55,38 @@ pub struct AccountCreationData{
gender: Box<str>, gender: Box<str>,
marketing_flag: YesNoVal, marketing_flag: YesNoVal,
off_device_flag: YesNoVal, off_device_flag: YesNoVal,
region: i32 region: i32,
} }
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename(serialize = "person"))] #[serde(rename(serialize = "person"))]
pub struct AccountCreationResponseData{ pub struct AccountCreationResponseData {
pid: i32 pid: i32,
} }
#[post("/v1/api/people", data="<data>")] #[post("/v1/api/people", data = "<data>")]
pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationData>) -> Result<Xml<AccountCreationResponseData>, Option<Errors<'_>>>{ pub async fn create_account(
database: &State<Pool>,
data: Xml<AccountCreationData>,
) -> Result<Xml<AccountCreationResponseData>, Option<Errors<'_>>> {
let database = database.inner(); let database = database.inner();
let nex_password = generate_nex_password();
let mut client = grpc_client().await.expect("unable to connect to grpc");
let Ok(ret) = client
.stage_new_account(ActStageInfo {
password: nex_password.clone().into_bytes(),
})
.await
else {
return Err(Some(DATABASE_ERROR));
};
let ActStageReturn { pid, .. } = ret.into_inner();
// its fine to crash here if we cant get the next pid as that is in my opinion a dead state // its fine to crash here if we cant get the next pid as that is in my opinion a dead state
// anyways as noone can register anymore, EVER // anyways as noone can register anymore, EVER
let pid = next_pid(database).await;
let verification_code: i32 = rand::rng().random_range(100_000..1_000_000); let verification_code: i32 = rand::rng().random_range(100_000..1_000_000);
let AccountCreationData { let AccountCreationData {
@ -82,14 +95,8 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
birth_date, birth_date,
tz_name, tz_name,
language, language,
email: Email{ email: Email { address },
address mii: Mii { name, data, .. },
},
mii: Mii{
name,
data,
..
},
marketing_flag, marketing_flag,
gender, gender,
region, region,
@ -98,17 +105,16 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
.. ..
} = data.0; } = data.0;
let account_level = if user_id.to_lowercase().contains("omey"){ let account_level = if user_id.to_lowercase().contains("omey") {
-1 -1
} else { } else {
0 0
}; };
let password = generate_password(pid, &password).ok_or(None)?; let password = generate_password(pid, &password).ok_or(None)?;
let nex_password = generate_nex_password();
sqlx::query!(" sqlx::query!(
"
INSERT INTO users ( INSERT INTO users (
pid, pid,
username, username,
@ -146,19 +152,20 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
verification_code, verification_code,
account_level, account_level,
nex_password nex_password
).execute(database).await.unwrap(); )
.execute(database)
.await
.unwrap();
//generate_s3_images(pid, &data).await; //generate_s3_images(pid, &data).await;
if let Err(e) = send_verification_email(address.as_ref(), verification_code, user_id.as_ref()).await { if let Err(e) =
send_verification_email(address.as_ref(), verification_code, user_id.as_ref()).await
{
println!("Failed to send verification email: {e}"); println!("Failed to send verification email: {e}");
} }
Ok( Ok(Xml(AccountCreationResponseData { pid }))
Xml(AccountCreationResponseData{
pid
})
)
} }
// #[derive(Serialize)] // #[derive(Serialize)]
@ -167,7 +174,7 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
// } // }
#[derive(Serialize)] #[derive(Serialize)]
struct EmailInfoOwnProfileData{ struct EmailInfoOwnProfileData {
address: String, address: String,
id: u32, id: u32,
parent: YesNoVal, parent: YesNoVal,
@ -177,11 +184,11 @@ struct EmailInfoOwnProfileData{
email_type: String, email_type: String,
updated_by: String, updated_by: String,
validated: YesNoVal, validated: YesNoVal,
validated_date: Option<NaiveDateTime> validated_date: Option<NaiveDateTime>,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct EmailInfoOwnOAuthProfileData{ struct EmailInfoOwnOAuthProfileData {
address: String, address: String,
id: u32, id: u32,
parent: bool, parent: bool,
@ -191,40 +198,37 @@ struct EmailInfoOwnOAuthProfileData{
email_type: String, email_type: String,
updated_by: String, updated_by: String,
validated: bool, validated: bool,
validated_date: Option<NaiveDateTime> validated_date: Option<NaiveDateTime>,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct MiiImage{ struct MiiImage {
cached_url: String, cached_url: String,
id: u32, id: u32,
url: String, url: String,
#[serde(rename = "type")] #[serde(rename = "type")]
image_type: String image_type: String,
}
#[derive(Serialize)]
struct MiiImages{
mii_image: MiiImage
} }
#[derive(Serialize)] #[derive(Serialize)]
struct MiiDataOwnProfileData{ struct MiiImages {
mii_image: MiiImage,
}
#[derive(Serialize)]
struct MiiDataOwnProfileData {
status: String, status: String,
data: String, data: String,
id: u32, id: u32,
mii_hash: String, mii_hash: String,
mii_images: MiiImages, mii_images: MiiImages,
name: String, name: String,
primary: YesNoVal primary: YesNoVal,
} }
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename(serialize = "person"))] #[serde(rename(serialize = "person"))]
pub struct GetOwnProfileData{ pub struct GetOwnProfileData {
active_flag: YesNoVal, active_flag: YesNoVal,
birth_date: NaiveDate, birth_date: NaiveDate,
country: String, country: String,
@ -246,7 +250,7 @@ pub struct GetOwnProfileData{
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename(serialize = "person"))] #[serde(rename(serialize = "person"))]
pub struct GetOwnOAuthProfileData{ pub struct GetOwnOAuthProfileData {
active_flag: bool, active_flag: bool,
birth_date: NaiveDate, birth_date: NaiveDate,
country: String, country: String,
@ -270,17 +274,17 @@ pub struct GetOwnOAuthProfileData{
} }
#[get("/v1/api/people/@me/profile")] #[get("/v1/api/people/@me/profile")]
pub fn get_own_profile(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>>{ pub fn get_own_profile(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>> {
Ds(Xml(build_profile(user.into()))) Ds(Xml(build_profile(user.into())))
} }
#[get("/v1/api/people/@me/devices/owner")] #[get("/v1/api/people/@me/devices/owner")]
pub fn get_device_owner(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>>{ pub fn get_device_owner(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>> {
Ds(Xml(build_profile(user.into()))) Ds(Xml(build_profile(user.into())))
} }
#[post("/v1/api/people/@me/devices")] #[post("/v1/api/people/@me/devices")]
pub fn get_own_device(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>>{ pub fn get_own_device(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>> {
Ds(Xml(build_profile(user.into()))) Ds(Xml(build_profile(user.into())))
} }
@ -314,7 +318,6 @@ pub fn build_profile(user: User) -> GetOwnProfileData {
.replace("\r", "") .replace("\r", "")
.replace(" ", ""); .replace(" ", "");
GetOwnProfileData { GetOwnProfileData {
active_flag: YesNoVal(true), active_flag: YesNoVal(true),
pid, pid,
@ -341,7 +344,7 @@ pub fn build_profile(user: User) -> GetOwnProfileData {
mii: MiiDataOwnProfileData { mii: MiiDataOwnProfileData {
id: gxhash32(mii_data.as_bytes(), 0), id: gxhash32(mii_data.as_bytes(), 0),
mii_hash: hex::encode(bytemuck::bytes_of( mii_hash: hex::encode(bytemuck::bytes_of(
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000)) &(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000)),
)), )),
name: crate::mii_util::MiiData::read(&mii_data) name: crate::mii_util::MiiData::read(&mii_data)
.map(|v| v.name) .map(|v| v.name)
@ -359,8 +362,8 @@ pub fn build_profile(user: User) -> GetOwnProfileData {
url: image_url.clone(), url: image_url.clone(),
cached_url: image_url, cached_url: image_url,
} }
} },
} },
}, },
off_device_flag: YesNoVal(off_device_allowed), off_device_flag: YesNoVal(off_device_allowed),
region, region,
@ -404,7 +407,6 @@ pub fn build_oauth_profile(user: User) -> GetOwnOAuthProfileData {
.replace("\r", "") .replace("\r", "")
.replace(" ", ""); .replace(" ", "");
GetOwnOAuthProfileData { GetOwnOAuthProfileData {
id, id,
sub, sub,
@ -434,7 +436,7 @@ pub fn build_oauth_profile(user: User) -> GetOwnOAuthProfileData {
mii: MiiDataOwnProfileData { mii: MiiDataOwnProfileData {
id: gxhash32(mii_data.as_bytes(), 0), id: gxhash32(mii_data.as_bytes(), 0),
mii_hash: hex::encode(bytemuck::bytes_of( mii_hash: hex::encode(bytemuck::bytes_of(
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000)) &(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000)),
)), )),
name: crate::mii_util::MiiData::read(&mii_data) name: crate::mii_util::MiiData::read(&mii_data)
.map(|v| v.name) .map(|v| v.name)
@ -452,8 +454,8 @@ pub fn build_oauth_profile(user: User) -> GetOwnOAuthProfileData {
url: image_url.clone(), url: image_url.clone(),
cached_url: image_url, cached_url: image_url,
} }
} },
} },
}, },
off_device_flag: off_device_allowed, off_device_flag: off_device_allowed,
region, region,
@ -492,8 +494,5 @@ pub async fn change_mii(
Ok(()) Ok(())
} }
#[post("/v1/api/people/@me/agreements")] #[post("/v1/api/people/@me/agreements")]
pub async fn thing(){ pub async fn thing() {}
}

View file

@ -1,5 +1,6 @@
#![deprecated = "handled by nex-account now"]
/*
use crate::Pool; use crate::Pool;
pub async fn next_pid(pool: &Pool) -> i32{ pub async fn next_pid(pool: &Pool) -> i32{
loop { loop {
let next_pid = sqlx::query!("SELECT nextval('pid_counter') as pid") let next_pid = sqlx::query!("SELECT nextval('pid_counter') as pid")
@ -27,3 +28,4 @@ pub async fn next_pid(pool: &Pool) -> i32{
} }
*/

View file

@ -1,69 +1,65 @@
use std::net::Ipv4Addr; use crate::Pool;
use rocket::{get, State};
use serde::Serialize;
use sqlx::types::ipnetwork::IpNetwork::V4;
use crate::account::account::Auth; use crate::account::account::Auth;
use crate::error::{Error, Errors}; use crate::error::{Error, Errors};
use crate::nnid::oauth::generate_token::{create_token}; use crate::nnid::oauth::generate_token::create_token;
use crate::nnid::oauth::generate_token::token_type::NEX_TOKEN; use crate::nnid::oauth::generate_token::token_type::NEX_TOKEN;
use crate::Pool;
use crate::xml::Xml; use crate::xml::Xml;
use nex_account::grpc::Pid;
use reqwest::header::SERVER;
use rocket::{State, get};
use serde::Serialize;
use sqlx::types::ipnetwork::IpNetwork::V4;
use std::net::Ipv4Addr;
const NO_IPV4_ERROR: Errors = Errors{ const NO_IPV4_ERROR: Errors = Errors {
error: &[ error: &[Error {
Error{
code: "1022", code: "1022",
message: "Server is not a valid IPv4 address" message: "Server is not a valid IPv4 address",
} }],
]
}; };
const SERVER_ERROR: Errors = Errors{ const SERVER_ERROR: Errors = Errors {
error: &[ error: &[Error {
Error{
code: "9999", code: "9999",
message: "Internal Server Error" message: "Internal Server Error",
} }],
]
}; };
const NO_SERVER_ERROR: Errors = Errors{ const NO_SERVER_ERROR: Errors = Errors {
error: &[ error: &[Error {
Error{
code: "1021", code: "1021",
message: "The requested game server was not found" message: "The requested game server was not found",
} }],
]
}; };
const MAINTENANCE_ERROR: Errors = Errors{ const MAINTENANCE_ERROR: Errors = Errors {
error: &[ error: &[Error {
Error{
code: "2002", code: "2002",
message: "The requested game server is under maintenance" message: "The requested game server is under maintenance",
} }],
]
}; };
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename = "nex_token")] #[serde(rename = "nex_token")]
pub struct NexToken{ pub struct NexToken {
host: Ipv4Addr, host: Ipv4Addr,
nex_password: String, nex_password: String,
pid: i32, pid: i32,
port: u16, port: u16,
token: String token: Box<str>,
} }
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename = "service_token")] #[serde(rename = "service_token")]
pub struct ServiceToken{ pub struct ServiceToken {
token: String token: String,
} }
#[get("/v1/api/provider/service_token/@me")] #[get("/v1/api/provider/service_token/@me")]
pub async fn get_service_token(pool: &State<Pool>, auth: Auth<true, false>) -> Result<Xml<ServiceToken>, Option<Errors<'static>>>{ pub async fn get_service_token(
pool: &State<Pool>,
auth: Auth<true, false>,
) -> Result<Xml<ServiceToken>, Option<Errors<'static>>> {
// just gonna put this here as a side note for the future: // 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 // we could also be using key derivation to derive the nex token as if it were a key
// that way we could reduce the data the database needs to store and also reduce the transfer // that way we could reduce the data the database needs to store and also reduce the transfer
@ -75,17 +71,15 @@ pub async fn get_service_token(pool: &State<Pool>, auth: Auth<true, false>) -> R
let token = create_token(pool, auth.pid, NEX_TOKEN, None).await; let token = create_token(pool, auth.pid, NEX_TOKEN, None).await;
Ok( Ok(Xml(ServiceToken { token }))
Xml(
ServiceToken{
token
}
)
)
} }
#[get("/v1/api/provider/nex_token/@me?<game_server_id>")] #[get("/v1/api/provider/nex_token/@me?<game_server_id>")]
pub async fn get_nex_token(pool: &State<Pool>, auth: Auth<true, false>, game_server_id: &str) -> Result<Xml<NexToken>, Option<Errors<'static>>>{ pub async fn get_nex_token(
pool: &State<Pool>,
auth: Auth<true, false>,
game_server_id: &str,
) -> Result<Xml<NexToken>, Option<Errors<'static>>> {
// just gonna put this here as a side note for the future: // 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 // we could also be using key derivation to derive the nex token as if it were a key
// that way we could reduce the data the database needs to store and also reduce the transfer // that way we could reduce the data the database needs to store and also reduce the transfer
@ -110,26 +104,35 @@ pub async fn get_nex_token(pool: &State<Pool>, auth: Auth<true, false>, game_ser
}; // only crash on db failure (not missing row) }; // only crash on db failure (not missing row)
if server.maintenance_mode { if server.maintenance_mode {
return Err(Some(MAINTENANCE_ERROR)) return Err(Some(MAINTENANCE_ERROR));
} }
let token = create_token(pool, auth.pid, NEX_TOKEN, None).await;
let V4(host) = server.address else { let V4(host) = server.address else {
return Err(Some(NO_IPV4_ERROR)); return Err(Some(NO_IPV4_ERROR));
}; };
let host = host.ip(); let host = host.ip();
Ok( let mut client = nex_account::grpc_client().await.unwrap();
Xml( let Ok(key) = client.get_nex_key_by_pid(Pid { pid: auth.pid }).await else {
NexToken{ println!("account does not exist on nex-account server");
return Err(Some(SERVER_ERROR));
};
let token = nex_account::gen_nexact_token(
auth.pid,
key.into_inner()
.key
.try_into()
.map_err(|_| Some(SERVER_ERROR))?,
)
.expect("NEX_ACCOUNT_KEYPAIR not set");
Ok(Xml(NexToken {
host, host,
port: server.port as u16, port: server.port as u16,
nex_password: auth.nex_password.clone(), nex_password: auth.nex_password.clone(),
pid: auth.pid, pid: auth.pid,
token token,
} }))
)
)
} }