Compare commits

...
Author SHA1 Message Date
c47a3fb5fc fix accidentally forcing nex-accounts to be fully created before using nex provider 2026-07-07 08:13:42 +02:00
4970f09db5 Merge pull request 'Update Rust crate p256 to 0.14.0' (#69) from renovate/p256-0.x into main
Reviewed-on: spacebar/account#69
2026-07-06 18:32:54 +02:00
437ab17e81 Merge pull request 'Update actions/cache action to v6' (#63) from renovate/actions-cache-6.x into main
Reviewed-on: spacebar/account#63
2026-07-06 18:32:48 +02:00
a19d45a4ac Merge pull request 'Update Rust crate quick-xml to 0.41.0' (#65) from renovate/quick-xml-0.x into main
Reviewed-on: spacebar/account#65
2026-07-06 18:32:42 +02:00
35a3e27591 Update Rust crate quick-xml to 0.41.0 2026-07-05 20:31:27 +00:00
ace476b66b Update Rust crate p256 to 0.14.0 2026-07-05 20:31:12 +00:00
2c7767124e switch to using nex-account for nex account management 2026-07-05 22:13:51 +02:00
3c3b8bf91c Merge pull request 'Fix default account level' (#70) from fix-tester-bug into main
Reviewed-on: spacebar/account#70
2026-07-05 11:14:07 +02:00
18da6e247b Fix default account level 2026-07-05 01:47:37 +02:00
f7d6b3ebf0 sqlx prepare 2026-07-04 20:52:24 +02:00
ea5f9d1c13 simple ban system for admins (to be replaced later) 2026-07-04 19:26:28 +02:00
cfd4bc21e3 fix signups because i am a professional dumbass 2026-06-27 09:26:51 +02:00
2dd404a957 Update actions/cache action to v6 2026-06-23 15:30:19 +00:00
2f1cb63e1e merge 2026-06-21 23:20:13 +02:00
381610c2ec allow website to not use a client secret 2026-06-21 23:19:41 +02:00
cec6414bc1 Update actions/checkout action to v7 2026-06-18 15:15:18 +00:00
52f044a51f Update Rust crate bcrypt to v0.19.2 2026-06-16 09:45:37 +00:00
14 changed files with 1663 additions and 1187 deletions

View file

@ -1,2 +1,5 @@
[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/" }

View file

@ -15,12 +15,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
submodules: recursive
- name: Cache container storage
uses: actions/cache@v5
uses: actions/cache@v6
with:
path: |
/var/lib/containers/storage

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET account_level = $1 WHERE username = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Text"
]
},
"nullable": []
},
"hash": "9d41e04076235e9a8c857c58c6f5fe7b26cba4da50ce8527ba32128152a31ce6"
}

View file

@ -1,64 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT game_server_id, maintenance_mode, address, port FROM nex_servers WHERE title_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "game_server_id",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "nex_servers",
"name": "game_server_id"
}
}
},
{
"ordinal": 1,
"name": "maintenance_mode",
"type_info": "Bool",
"origin": {
"Table": {
"table": "nex_servers",
"name": "maintenance_mode"
}
}
},
{
"ordinal": 2,
"name": "address",
"type_info": "Inet",
"origin": {
"Table": {
"table": "nex_servers",
"name": "address"
}
}
},
{
"ordinal": 3,
"name": "port",
"type_info": "Int4",
"origin": {
"Table": {
"table": "nex_servers",
"name": "port"
}
}
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "e3349c0e5ab82bbef359cf573caf454f23588f3cfa27299e58863dc9397d55de"
}

2239
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,7 @@ incremental = false
rocket = { version = "0.5.1", features = ["json"] }
serde = { version = "1.0.218", features = ["derive"] }
log = "0.4.26"
quick-xml = { version = "0.40.0", features = ["serialize"] }
quick-xml = { version = "0.41.0", features = ["serialize"] }
tokio = "1.43.0"
dotenvy = "0.15.7"
once_cell = "1.20.3"
@ -49,9 +49,10 @@ reqwest = "0.13.0"
binrw = "0.15.1"
ecdsa = { version = "0.16.9", features = ["pem", "std", "verifying"] }
sha256 = "1.6.0"
p256 = "0.13.2"
p256 = "0.14.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"] }
nex-account = { version = "0.2.4", registry = "spbr" }

View file

@ -0,0 +1,39 @@
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)]
pub struct AdminRequest<'r> {
pub username: &'r str,
}
#[post("/api/v2/admin/ban", data = "<request>")]
pub async fn ban_user(pool: &State<Pool>, auth: Auth<true>, request: Form<AdminRequest<'_>>) -> Result<(), Status> {
if auth.account_level < 2 {
return Err(Status::Forbidden);
};
log::info!("banning user {:?} from moderator {:?}", request.username, auth.username);
let row = sqlx::query!(
"UPDATE users SET account_level = $1 WHERE username = $2",
-1,
request.username
)
.execute(pool.inner())
.await
.map_err(|e| {
log::error!("failed to execute query: {:?}", e);
return Err::<(), rocket::http::Status>(Status::InternalServerError);
});
log::info!("banned user {:?}", request.username);
Ok(())
}

View file

@ -0,0 +1 @@
pub mod bans;

View file

@ -1,2 +1,3 @@
pub mod oauth;
pub mod users;
pub mod users;
pub mod admin;

View file

@ -41,10 +41,7 @@ pub fn verify_nintendo_password(pid: i32, text_password: &str, db_bcrypt_hash: &
sha.update(&[0x02, 0x65, 0x43, 0x46]);
sha.update(text_password.as_bytes());
let hashed_password_hex = hex::encode(sha.finalize());
match bcrypt::verify(hashed_password_hex, db_bcrypt_hash) {
Ok(valid) => valid,
Err(_) => false,
}
bcrypt::verify(hashed_password_hex, db_bcrypt_hash).unwrap_or_else(|_| false)
}
// dummy error responses
@ -59,19 +56,25 @@ pub async fn generate_token(
form_data: Form<TokenRequest<'_>>
) -> Result<Json<OAuthTokenResponse>, (Status, Json<OAuthErrorResponse>)> {
if form_data.client_id != Some("account") {
return Err((
Status::BadRequest,
Json(OAuthErrorResponse { error: "invalid_client".to_string() })
));
}
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() })
));
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::BadRequest,
Json(OAuthErrorResponse { error: "invalid_client".to_string() })
));
}
}
// i'm only supporting the password grant incase someone feels lazy.
@ -163,4 +166,4 @@ pub async fn generate_token(
token_type: "Bearer".to_string(),
expires_in: 3600,
}))
}
}

View file

@ -118,6 +118,7 @@ async fn launch() -> _ {
json_api::users::delete::delete_account,
json_api::oauth::authorize::authorize_page,
json_api::oauth::authorize::authorize_submit,
json_api::admin::bans::ban_user,
nnid::people::thing,
// graphql::graphiql,
// graphql::playground,

View file

@ -1,42 +1,41 @@
#![allow(unused)]
use chrono::{NaiveDate, NaiveDateTime};
use gxhash::{gxhash32, gxhash64};
use rocket::{get, post, put, State};
use rocket::serde::{Deserialize, Serialize};
use crate::Pool;
use crate::account::account::{Auth, User, generate_nex_password, generate_password};
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 rand::prelude::*;
use crate::error::{Error, Errors};
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{
error: &[
Error{
code: "9999",
message: "Internal server error"
}
]
const DATABASE_ERROR: Errors = Errors {
error: &[Error {
code: "9999",
message: "Internal server error",
}],
};
#[derive(Deserialize)]
pub struct Email{
address: Box<str>
pub struct Email {
address: Box<str>,
}
#[derive(Deserialize)]
pub struct UpdateMiiData {
_name: Box<str>,
_primary: crate::xml::YesNoVal,
name: Box<str>,
primary: crate::xml::YesNoVal,
data: Box<str>,
}
#[derive(Deserialize, Serialize)]
pub struct Mii{
pub struct Mii {
name: Box<str>,
primary: YesNoVal,
data: Box<str>,
@ -44,7 +43,7 @@ pub struct Mii{
#[derive(Deserialize)]
#[serde(rename(serialize = "person"))]
pub struct AccountCreationData{
pub struct AccountCreationData {
birth_date: NaiveDate,
user_id: Box<str>,
password: Box<str>,
@ -56,24 +55,38 @@ pub struct AccountCreationData{
gender: Box<str>,
marketing_flag: YesNoVal,
off_device_flag: YesNoVal,
region: i32
region: i32,
}
#[derive(Serialize)]
#[serde(rename(serialize = "person"))]
pub struct AccountCreationResponseData{
pid: i32
pub struct AccountCreationResponseData {
pid: i32,
}
#[post("/v1/api/people", data="<data>")]
pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationData>) -> Result<Xml<AccountCreationResponseData>, Option<Errors<'_>>>{
#[post("/v1/api/people", data = "<data>")]
pub async fn create_account(
database: &State<Pool>,
data: Xml<AccountCreationData>,
) -> Result<Xml<AccountCreationResponseData>, Option<Errors<'_>>> {
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
// 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 AccountCreationData {
@ -82,14 +95,8 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
birth_date,
tz_name,
language,
email: Email{
address
},
mii: Mii{
name,
data,
..
},
email: Email { address },
mii: Mii { name, data, .. },
marketing_flag,
gender,
region,
@ -98,17 +105,16 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
..
} = data.0;
let account_level = if user_id.to_lowercase().contains("omey"){
let account_level = if user_id.to_lowercase().contains("omey") {
-1
} else {
1
0
};
let password = generate_password(pid, &password).ok_or(None)?;
let nex_password = generate_nex_password();
sqlx::query!("
sqlx::query!(
"
INSERT INTO users (
pid,
username,
@ -146,19 +152,20 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
verification_code,
account_level,
nex_password
).execute(database).await.unwrap();
)
.execute(database)
.await
.unwrap();
//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}");
}
Ok(
Xml(AccountCreationResponseData{
pid
})
)
Ok(Xml(AccountCreationResponseData { pid }))
}
// #[derive(Serialize)]
@ -167,7 +174,7 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
// }
#[derive(Serialize)]
struct EmailInfoOwnProfileData{
struct EmailInfoOwnProfileData {
address: String,
id: u32,
parent: YesNoVal,
@ -177,11 +184,11 @@ struct EmailInfoOwnProfileData{
email_type: String,
updated_by: String,
validated: YesNoVal,
validated_date: Option<NaiveDateTime>
validated_date: Option<NaiveDateTime>,
}
#[derive(Serialize)]
struct EmailInfoOwnOAuthProfileData{
struct EmailInfoOwnOAuthProfileData {
address: String,
id: u32,
parent: bool,
@ -191,40 +198,37 @@ struct EmailInfoOwnOAuthProfileData{
email_type: String,
updated_by: String,
validated: bool,
validated_date: Option<NaiveDateTime>
validated_date: Option<NaiveDateTime>,
}
#[derive(Serialize)]
struct MiiImage{
struct MiiImage {
cached_url: String,
id: u32,
url: String,
#[serde(rename = "type")]
image_type: String
}
#[derive(Serialize)]
struct MiiImages{
mii_image: MiiImage
image_type: String,
}
#[derive(Serialize)]
struct MiiDataOwnProfileData{
struct MiiImages {
mii_image: MiiImage,
}
#[derive(Serialize)]
struct MiiDataOwnProfileData {
status: String,
data: String,
id: u32,
mii_hash: String,
mii_images: MiiImages,
name: String,
primary: YesNoVal
primary: YesNoVal,
}
#[derive(Serialize)]
#[serde(rename(serialize = "person"))]
pub struct GetOwnProfileData{
pub struct GetOwnProfileData {
active_flag: YesNoVal,
birth_date: NaiveDate,
country: String,
@ -246,7 +250,7 @@ pub struct GetOwnProfileData{
#[derive(Serialize)]
#[serde(rename(serialize = "person"))]
pub struct GetOwnOAuthProfileData{
pub struct GetOwnOAuthProfileData {
active_flag: bool,
birth_date: NaiveDate,
country: String,
@ -270,17 +274,17 @@ pub struct GetOwnOAuthProfileData{
}
#[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())))
}
#[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())))
}
#[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())))
}
@ -314,59 +318,58 @@ pub fn build_profile(user: User) -> GetOwnProfileData {
.replace("\r", "")
.replace(" ", "");
GetOwnProfileData {
active_flag: YesNoVal(true),
pid,
user_id: username,
gender,
birth_date: birthdate,
country,
create_date: creation_date,
tz_name: timezone,
language,
updated,
marketing_flag: YesNoVal(marketing_allowed),
email: EmailInfoOwnProfileData {
id: gxhash32(email.as_bytes(), 0),
address: email,
validated: YesNoVal(email_verified_since.is_some()),
validated_date: email_verified_since,
email_type: "DEFAULT".to_string(),
updated_by: "USER".to_string(),
reachable: YesNoVal(true),
primary: YesNoVal(true),
parent: YesNoVal(false),
},
mii: MiiDataOwnProfileData {
id: gxhash32(mii_data.as_bytes(), 0),
mii_hash: hex::encode(bytemuck::bytes_of(
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000))
)),
name: crate::mii_util::MiiData::read(&mii_data)
GetOwnProfileData {
active_flag: YesNoVal(true),
pid,
user_id: username,
gender,
birth_date: birthdate,
country,
create_date: creation_date,
tz_name: timezone,
language,
updated,
marketing_flag: YesNoVal(marketing_allowed),
email: EmailInfoOwnProfileData {
id: gxhash32(email.as_bytes(), 0),
address: email,
validated: YesNoVal(email_verified_since.is_some()),
validated_date: email_verified_since,
email_type: "DEFAULT".to_string(),
updated_by: "USER".to_string(),
reachable: YesNoVal(true),
primary: YesNoVal(true),
parent: YesNoVal(false),
},
mii: MiiDataOwnProfileData {
id: gxhash32(mii_data.as_bytes(), 0),
mii_hash: hex::encode(bytemuck::bytes_of(
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000)),
)),
name: crate::mii_util::MiiData::read(&mii_data)
.map(|v| v.name)
.unwrap_or_else(|| "INVALID".to_string()),
primary: YesNoVal(true),
data: mii_data,
status: "COMPLETED".to_string(),
mii_images: MiiImages {
mii_image: {
let image_url = get_mii_img_url(pid, "tga");
let url_hash = gxhash32(image_url.as_bytes(), 0);
MiiImage {
image_type: "standard".to_string(),
id: url_hash,
url: image_url.clone(),
cached_url: image_url,
}
primary: YesNoVal(true),
data: mii_data,
status: "COMPLETED".to_string(),
mii_images: MiiImages {
mii_image: {
let image_url = get_mii_img_url(pid, "tga");
let url_hash = gxhash32(image_url.as_bytes(), 0);
MiiImage {
image_type: "standard".to_string(),
id: url_hash,
url: image_url.clone(),
cached_url: image_url,
}
}
},
},
off_device_flag: YesNoVal(off_device_allowed),
region,
utc_offset: timezone_offset,
account_level,
}
},
off_device_flag: YesNoVal(off_device_allowed),
region,
utc_offset: timezone_offset,
account_level,
}
}
pub fn build_oauth_profile(user: User) -> GetOwnOAuthProfileData {
@ -404,62 +407,61 @@ pub fn build_oauth_profile(user: User) -> GetOwnOAuthProfileData {
.replace("\r", "")
.replace(" ", "");
GetOwnOAuthProfileData {
id,
sub,
uid,
active_flag: true,
pid,
user_id: username,
gender,
birth_date: birthdate,
country,
create_date: creation_date,
tz_name: timezone,
language,
updated,
marketing_flag: marketing_allowed,
email: EmailInfoOwnOAuthProfileData {
id: gxhash32(email.as_bytes(), 0),
address: email,
validated: email_verified_since.is_some(),
validated_date: email_verified_since,
email_type: "DEFAULT".to_string(),
updated_by: "USER".to_string(),
reachable: true,
primary: true,
parent: false,
},
mii: MiiDataOwnProfileData {
id: gxhash32(mii_data.as_bytes(), 0),
mii_hash: hex::encode(bytemuck::bytes_of(
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000))
)),
name: crate::mii_util::MiiData::read(&mii_data)
GetOwnOAuthProfileData {
id,
sub,
uid,
active_flag: true,
pid,
user_id: username,
gender,
birth_date: birthdate,
country,
create_date: creation_date,
tz_name: timezone,
language,
updated,
marketing_flag: marketing_allowed,
email: EmailInfoOwnOAuthProfileData {
id: gxhash32(email.as_bytes(), 0),
address: email,
validated: email_verified_since.is_some(),
validated_date: email_verified_since,
email_type: "DEFAULT".to_string(),
updated_by: "USER".to_string(),
reachable: true,
primary: true,
parent: false,
},
mii: MiiDataOwnProfileData {
id: gxhash32(mii_data.as_bytes(), 0),
mii_hash: hex::encode(bytemuck::bytes_of(
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000)),
)),
name: crate::mii_util::MiiData::read(&mii_data)
.map(|v| v.name)
.unwrap_or_else(|| "INVALID".to_string()),
primary: YesNoVal(true),
data: mii_data,
status: "COMPLETED".to_string(),
mii_images: MiiImages {
mii_image: {
let image_url = get_mii_img_url(pid, "png");
let url_hash = gxhash32(image_url.as_bytes(), 0);
MiiImage {
image_type: "standard".to_string(),
id: url_hash,
url: image_url.clone(),
cached_url: image_url,
}
primary: YesNoVal(true),
data: mii_data,
status: "COMPLETED".to_string(),
mii_images: MiiImages {
mii_image: {
let image_url = get_mii_img_url(pid, "png");
let url_hash = gxhash32(image_url.as_bytes(), 0);
MiiImage {
image_type: "standard".to_string(),
id: url_hash,
url: image_url.clone(),
cached_url: image_url,
}
}
},
},
off_device_flag: off_device_allowed,
region,
utc_offset: timezone_offset,
account_level,
}
},
off_device_flag: off_device_allowed,
region,
utc_offset: timezone_offset,
account_level,
}
}
#[put("/v1/api/people/@me/miis/@primary", data = "<data>")]
@ -479,8 +481,8 @@ pub async fn change_mii(
mii_data,
pid
)
.execute(db)
.await;
.execute(db)
.await;
if let Err(e) = result {
println!("Failed to update Mii data for PID {}: {:?}", pid, e);
@ -492,8 +494,5 @@ pub async fn change_mii(
Ok(())
}
#[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;
pub async fn next_pid(pool: &Pool) -> i32{
loop {
let next_pid = sqlx::query!("SELECT nextval('pid_counter') as pid")
@ -26,4 +27,5 @@ pub async fn next_pid(pool: &Pool) -> i32{
}
}
*/

View file

@ -1,69 +1,65 @@
use std::net::Ipv4Addr;
use rocket::{get, State};
use serde::Serialize;
use sqlx::types::ipnetwork::IpNetwork::V4;
use crate::Pool;
use crate::account::account::Auth;
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::Pool;
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{
error: &[
Error{
code: "1022",
message: "Server is not a valid IPv4 address"
}
]
const NO_IPV4_ERROR: Errors = Errors {
error: &[Error {
code: "1022",
message: "Server is not a valid IPv4 address",
}],
};
const SERVER_ERROR: Errors = Errors{
error: &[
Error{
code: "9999",
message: "Internal Server Error"
}
]
const SERVER_ERROR: Errors = Errors {
error: &[Error {
code: "9999",
message: "Internal Server Error",
}],
};
const NO_SERVER_ERROR: Errors = Errors{
error: &[
Error{
code: "1021",
message: "The requested game server was not found"
}
]
const NO_SERVER_ERROR: Errors = Errors {
error: &[Error {
code: "1021",
message: "The requested game server was not found",
}],
};
const MAINTENANCE_ERROR: Errors = Errors{
error: &[
Error{
code: "2002",
message: "The requested game server is under maintenance"
}
]
const MAINTENANCE_ERROR: Errors = Errors {
error: &[Error {
code: "2002",
message: "The requested game server is under maintenance",
}],
};
#[derive(Serialize)]
#[serde(rename = "nex_token")]
pub struct NexToken{
pub struct NexToken {
host: Ipv4Addr,
nex_password: String,
pid: i32,
port: u16,
token: String
token: Box<str>,
}
#[derive(Serialize)]
#[serde(rename = "service_token")]
pub struct ServiceToken{
token: String
pub struct ServiceToken {
token: String,
}
#[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:
// 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
@ -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;
Ok(
Xml(
ServiceToken{
token
}
)
)
Ok(Xml(ServiceToken { token }))
}
#[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:
// 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
@ -108,12 +102,10 @@ pub async fn get_nex_token(pool: &State<Pool>, auth: Auth<true, false>, game_ser
Some(row) => row,
None => return Err(Some(NO_SERVER_ERROR)),
}; // only crash on db failure (not missing row)
if server.maintenance_mode {
return Err(Some(MAINTENANCE_ERROR))
}
let token = create_token(pool, auth.pid, NEX_TOKEN, None).await;
if server.maintenance_mode {
return Err(Some(MAINTENANCE_ERROR));
}
let V4(host) = server.address else {
return Err(Some(NO_IPV4_ERROR));
@ -121,15 +113,29 @@ pub async fn get_nex_token(pool: &State<Pool>, auth: Auth<true, false>, game_ser
let host = host.ip();
Ok(
Xml(
NexToken{
host,
port: server.port as u16,
nex_password: auth.nex_password.clone(),
pid: auth.pid,
token
}
)
let mut client = nex_account::grpc_client().await.unwrap();
let Ok(key) = client
.get_nex_key_by_pid_maybe_staged(Pid { pid: auth.pid })
.await
else {
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,
port: server.port as u16,
nex_password: auth.nex_password.clone(),
pid: auth.pid,
token,
}))
}