Merge branch 'feat/website-api' into 'main'

Add Pretendo API

See merge request perditum/account-rs!3
This commit is contained in:
andrea 2025-04-29 12:57:56 +00:00
commit c874781101
8 changed files with 230 additions and 4 deletions

View file

@ -12,7 +12,7 @@ stages:
- initialize-submodules - initialize-submodules
- build - build
- push - push
- test # for SAST + Dependency Scanning
build: build:
stage: build stage: build
@ -31,3 +31,22 @@ push:
only: only:
- main - main
sast:
stage: test
allow_failure: true
script: ['echo "Running SAST scan"']
artifacts:
reports:
sast: gl-sast-report.json
rules:
- if: $CI_COMMIT_BRANCH
dependency_scanning:
stage: test
allow_failure: true
script: ['echo "Running dep scan"']
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json
rules:
- if: $CI_COMMIT_BRANCH

1
Cargo.lock generated
View file

@ -2570,6 +2570,7 @@ dependencies = [
"rocket_codegen", "rocket_codegen",
"rocket_http", "rocket_http",
"serde", "serde",
"serde_json",
"state", "state",
"tempfile", "tempfile",
"time", "time",

View file

@ -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"] }

View file

@ -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,

View file

@ -170,6 +170,7 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
address address
}, },
mii: Mii{ mii: Mii{
name,
data, data,
.. ..
}, },
@ -376,8 +377,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
View 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,
}))
}

4
src/papi/mod.rs Normal file
View file

@ -0,0 +1,4 @@
#[deprecated(note="please use the upcoming api instead of this")]
pub mod user;
#[deprecated(note="please use the upcoming api instead of this")]
pub mod login;

107
src/papi/user.rs Normal file
View 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://cdn.spfn.cc/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,
},
},
})
}