feat(website): support the pretendo website API, PAPI for short
This commit is contained in:
parent
ee3a351259
commit
2168f6fec1
7 changed files with 209 additions and 3 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2570,6 +2570,7 @@ dependencies = [
|
||||||
"rocket_codegen",
|
"rocket_codegen",
|
||||||
"rocket_http",
|
"rocket_http",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"state",
|
"state",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"time",
|
"time",
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ lto = true
|
||||||
incremental = false
|
incremental = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = "0.5.1"
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
serde = { version = "1.0.218", features = ["derive"] }
|
serde = { version = "1.0.218", features = ["derive"] }
|
||||||
log = "0.4.26"
|
log = "0.4.26"
|
||||||
quick-xml = { version = "0.37.2", features = ["serialize"] }
|
quick-xml = { version = "0.37.2", features = ["serialize"] }
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ mod data_wrapper;
|
||||||
mod grpc;
|
mod grpc;
|
||||||
mod graphql;
|
mod graphql;
|
||||||
mod email;
|
mod email;
|
||||||
|
mod papi;
|
||||||
|
|
||||||
type Pool = sqlx::Pool<Postgres>;
|
type Pool = sqlx::Pool<Postgres>;
|
||||||
|
|
||||||
|
|
@ -167,6 +168,8 @@ async fn launch() -> _ {
|
||||||
nnid::provider::get_nex_token,
|
nnid::provider::get_nex_token,
|
||||||
nnid::provider::get_service_token,
|
nnid::provider::get_service_token,
|
||||||
nnid::mapped_ids::mapped_ids,
|
nnid::mapped_ids::mapped_ids,
|
||||||
|
papi::login::login,
|
||||||
|
papi::user::get_user,
|
||||||
//graphql
|
//graphql
|
||||||
graphql::graphiql,
|
graphql::graphiql,
|
||||||
graphql::playground,
|
graphql::playground,
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
|
||||||
address
|
address
|
||||||
},
|
},
|
||||||
mii: Mii{
|
mii: Mii{
|
||||||
|
name,
|
||||||
data,
|
data,
|
||||||
..
|
..
|
||||||
},
|
},
|
||||||
|
|
@ -218,6 +219,7 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
|
||||||
gender.as_ref(),
|
gender.as_ref(),
|
||||||
data.as_ref(),
|
data.as_ref(),
|
||||||
verification_code,
|
verification_code,
|
||||||
|
name.as_ref(),
|
||||||
).execute(database).await.unwrap();
|
).execute(database).await.unwrap();
|
||||||
|
|
||||||
generate_s3_images(pid, &data).await;
|
generate_s3_images(pid, &data).await;
|
||||||
|
|
@ -376,8 +378,8 @@ fn build_own_profile(user: User) -> Ds<Xml<GetOwnProfileData>> {
|
||||||
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000))
|
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000))
|
||||||
)),
|
)),
|
||||||
name: mii::MiiData::read(&mii_data)
|
name: mii::MiiData::read(&mii_data)
|
||||||
.map(|v| v.name)
|
.map(|v| v.name)
|
||||||
.unwrap_or_else(|| "INVALID".to_string()),
|
.unwrap_or_else(|| "INVALID".to_string()),
|
||||||
primary: YesNoVal(true),
|
primary: YesNoVal(true),
|
||||||
data: mii_data,
|
data: mii_data,
|
||||||
status: "COMPLETED".to_string(),
|
status: "COMPLETED".to_string(),
|
||||||
|
|
|
||||||
91
src/papi/login.rs
Normal file
91
src/papi/login.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
use rocket::{post, State};
|
||||||
|
use rocket::form::Form;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use crate::Pool;
|
||||||
|
use crate::account::account::{User, read_bearer_auth_token};
|
||||||
|
use crate::nnid::oauth::generate_token::{create_token, token_type::AUTH_TOKEN, token_type::AUTH_REFRESH_TOKEN};
|
||||||
|
use crate::error::{Error, Errors};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct LoginRequest {
|
||||||
|
grant_type: String,
|
||||||
|
username: Option<String>,
|
||||||
|
password: Option<String>,
|
||||||
|
refresh_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct LoginResponse {
|
||||||
|
access_token: String,
|
||||||
|
token_type: String,
|
||||||
|
expires_in: i32,
|
||||||
|
refresh_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const INVALID_GRANT_TYPE_ERROR: Errors<'static> = Errors {
|
||||||
|
error: &[Error {
|
||||||
|
code: "0100",
|
||||||
|
message: "Invalid grant type",
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
const ACCOUNT_ID_OR_PASSWORD_ERRORS: Errors<'static> = Errors {
|
||||||
|
error: &[Error {
|
||||||
|
code: "0106",
|
||||||
|
message: "Invalid account ID or password",
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
const INVALID_REFRESH_TOKEN_ERRORS: Errors<'static> = Errors {
|
||||||
|
error: &[Error {
|
||||||
|
code: "0107",
|
||||||
|
message: "Invalid or missing refresh token",
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
#[post("/v1/login", data = "<form_data>")]
|
||||||
|
pub async fn login(pool: &State<Pool>, form_data: Json<LoginRequest>) -> Result<Json<LoginResponse>, Option<Errors<'static>>> {
|
||||||
|
let pool = pool.inner();
|
||||||
|
let grant_type = form_data.grant_type.as_str();
|
||||||
|
|
||||||
|
if grant_type != "password" && grant_type != "refresh_token" {
|
||||||
|
return Err(Some(INVALID_GRANT_TYPE_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
if grant_type == "password" {
|
||||||
|
let username = form_data.username.as_ref().ok_or(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS))?;
|
||||||
|
let password = form_data.password.as_ref().ok_or(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS))?;
|
||||||
|
|
||||||
|
user = User::get_by_username(username, pool)
|
||||||
|
.await
|
||||||
|
.ok_or(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS))?;
|
||||||
|
|
||||||
|
if !user.verify_cleartext_password(password).is_some_and(|v| v) {
|
||||||
|
return Err(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let refresh_token = form_data.refresh_token.as_ref().ok_or(Some(INVALID_REFRESH_TOKEN_ERRORS))?;
|
||||||
|
|
||||||
|
user = read_bearer_auth_token(pool, refresh_token)
|
||||||
|
.await
|
||||||
|
.ok_or(Some(INVALID_REFRESH_TOKEN_ERRORS))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.account_level < 0 {
|
||||||
|
return Err(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS));
|
||||||
|
}
|
||||||
|
|
||||||
|
let access_token = create_token(pool, user.pid, AUTH_TOKEN, None).await;
|
||||||
|
let refresh_token = create_token(pool, user.pid, AUTH_REFRESH_TOKEN, None).await;
|
||||||
|
|
||||||
|
Ok(Json(LoginResponse {
|
||||||
|
access_token,
|
||||||
|
token_type: "Bearer".to_string(),
|
||||||
|
expires_in: 3600,
|
||||||
|
refresh_token,
|
||||||
|
}))
|
||||||
|
}
|
||||||
2
src/papi/mod.rs
Normal file
2
src/papi/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod user;
|
||||||
|
pub mod login;
|
||||||
107
src/papi/user.rs
Normal file
107
src/papi/user.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
use rocket::{get};
|
||||||
|
use crate::account::account::{Auth};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct EmailInfo {
|
||||||
|
address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct TimezoneInfo {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct MiiInfo {
|
||||||
|
data: String,
|
||||||
|
name: String,
|
||||||
|
image_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct FlagsInfo {
|
||||||
|
marketing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct ConnectionsInfo {
|
||||||
|
discord: DiscordInfo,
|
||||||
|
stripe: StripeInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct DiscordInfo {
|
||||||
|
id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct StripeInfo {
|
||||||
|
tier_name: Option<String>,
|
||||||
|
tier_level: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct UserInfoResponse {
|
||||||
|
deleted: bool,
|
||||||
|
access_level: i32,
|
||||||
|
server_access_level: String,
|
||||||
|
pid: i32,
|
||||||
|
creation_date: chrono::NaiveDateTime,
|
||||||
|
updated: chrono::NaiveDateTime,
|
||||||
|
username: String,
|
||||||
|
birthdate: chrono::NaiveDate,
|
||||||
|
gender: String,
|
||||||
|
country: String,
|
||||||
|
email: EmailInfo,
|
||||||
|
timezone: TimezoneInfo,
|
||||||
|
mii: MiiInfo,
|
||||||
|
flags: FlagsInfo,
|
||||||
|
connections: ConnectionsInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/v1/user")]
|
||||||
|
pub async fn get_user(auth: Auth<false>) -> Json<UserInfoResponse> {
|
||||||
|
let user = auth.0;
|
||||||
|
|
||||||
|
Json(UserInfoResponse {
|
||||||
|
deleted: false,
|
||||||
|
access_level: user.account_level,
|
||||||
|
server_access_level: "test".to_string(),
|
||||||
|
pid: user.pid,
|
||||||
|
creation_date: user.creation_date,
|
||||||
|
updated: user.updated,
|
||||||
|
username: user.username.clone(),
|
||||||
|
birthdate: user.birthdate,
|
||||||
|
gender: user.gender.clone(),
|
||||||
|
country: user.country.clone(),
|
||||||
|
email: EmailInfo {
|
||||||
|
address: user.email.clone(),
|
||||||
|
},
|
||||||
|
timezone: TimezoneInfo {
|
||||||
|
name: user.timezone.clone(),
|
||||||
|
},
|
||||||
|
mii: MiiInfo {
|
||||||
|
data: user.mii_data.clone(),
|
||||||
|
name: {
|
||||||
|
let cleaned = user.mii_data.replace('\n', "").replace('\r', "");
|
||||||
|
mii::MiiData::read(&cleaned)
|
||||||
|
.map(|v| v.name)
|
||||||
|
.unwrap_or_else(|| "INVALID".to_string())
|
||||||
|
},
|
||||||
|
image_url: format!("https://minio.spfn.cc:9000/act-rs/mii/{}/normal_face.png", user.pid),
|
||||||
|
},
|
||||||
|
flags: FlagsInfo {
|
||||||
|
marketing: user.marketing_allowed,
|
||||||
|
},
|
||||||
|
connections: ConnectionsInfo {
|
||||||
|
discord: DiscordInfo {
|
||||||
|
id: None,
|
||||||
|
},
|
||||||
|
stripe: StripeInfo {
|
||||||
|
tier_name: None,
|
||||||
|
tier_level: None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue