1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Create RC database layer

This commit is contained in:
NGnius (Graham)
2025-05-07 20:23:22 -04:00
parent 69c6c54650
commit 77ddeb7891
55 changed files with 2689 additions and 395 deletions

View File

@@ -16,8 +16,11 @@ serde_json.workspace = true
chrono = "0.4"
polariton_server.workspace = true
tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util", "time" ] }
async-trait.workspace = true
# auth
libfj.workspace = true
jsonwebtoken = "9"
argon2 = { version = "0.5", features = [ "std" ] }
rc_database = { version = "0.2", path = "../rc_database" }

View File

@@ -217,4 +217,43 @@ impl ItemCategory {
pub fn but_bigger(&self) -> i32 {
(*self as i32) * 100_000
}
pub fn from_bigger(num: i32) -> Option<Self> {
Self::from_smaller(num / 100_000)
}
pub fn from_smaller(num: i32) -> Option<Self> {
match num {
0 => Some(Self::NoFunction),
1 => Some(Self::Wheel),
2 => Some(Self::Hover),
3 => Some(Self::Wing),
4 => Some(Self::Rudder),
5 => Some(Self::Thruster),
6 => Some(Self::InsectLeg),
7 => Some(Self::MechLeg),
8 => Some(Self::Ski),
9 => Some(Self::TankTrack),
10 => Some(Self::Rotor),
11 => Some(Self::SprinterLeg),
12 => Some(Self::Propeller),
100 => Some(Self::Laser),
200 => Some(Self::Plasma),
250 => Some(Self::Mortar),
300 => Some(Self::Rail),
400 => Some(Self::Nano),
500 => Some(Self::Tesla),
600 => Some(Self::Aeroflak),
650 => Some(Self::Ion),
701 => Some(Self::Seeker),
750 => Some(Self::Chaingun),
800 => Some(Self::ShieldModule),
801 => Some(Self::GhostModule),
802 => Some(Self::BlinkModule),
803 => Some(Self::EmpModule),
804 => Some(Self::WindowmakerModule),
900 => Some(Self::EnergyModule),
_ => None,
}
}
}

View File

@@ -255,4 +255,10 @@ impl <C: Clone> super::ConfigProvider<C> for CubeConfig {
items: self.chat.public_channels.iter().map(|s| Typed::Str(s.into())).collect(),
})
}
fn server_config(&self) -> super::ServerConfig {
super::ServerConfig {
database: self.settings.server.database.clone(),
}
}
}

View File

@@ -2,7 +2,7 @@ mod cubes_json;
pub use cubes_json::CubeConfig;
mod traits;
pub use traits::{ConfigProvider, CompleteCampaignProvider, DevMessageProvider};
pub use traits::{ConfigProvider, CompleteCampaignProvider, DevMessageProvider, ServerConfig};
pub type ConfigImpl = CubeConfig;

View File

@@ -18,6 +18,7 @@ pub trait ConfigProvider<C: Clone> {
fn client_config(&self) -> Typed<C>;
fn login_messages(&self) -> DevMessageProvider<C>;
fn public_channels(&self) -> Typed<C>;
fn server_config(&self) -> ServerConfig;
}
pub struct CompleteCampaignProvider {
@@ -83,3 +84,7 @@ pub struct TypedDevMessage<C> {
pub message: Typed<C>,
pub display_time: Typed<C>,
}
pub struct ServerConfig {
pub database: String,
}

View File

@@ -99,6 +99,65 @@ impl std::convert::Into<crate::data::garage_bay::GarageSlotInfo> for GarageSlot
}
}
pub fn db_into_data(garage: rc_database::schema::garage::Model) -> crate::data::garage_bay::GarageSlotInfo {
crate::data::garage_bay::GarageSlotInfo {
name: garage.name,
cubes: 0, // TODO garage.cubes,
crf_id: garage.crf_id.unwrap_or(0),
was_rated: garage.was_rated,
movement_categories: movement_category_into_data(&garage.movement_categories),
uuid: i64_split(garage.uuid),
thumbnail_version: garage.thumbnail_version,
total_robot_cpu: garage.total_robot_cpu,
total_cosmetic_cpu: garage.total_cosmetic_cpu,
total_robot_ranking: garage.total_robot_ranking,
bay_cpu: garage.bay_cpu,
tutorial_robot: garage.tutorial_robot,
starter_robot_index: garage.starter_robot_index.map(|x| x as i32).unwrap_or(-1),
control_type: control_ty_into_data(garage.control_type),
control_options: crate::data::garage_bay::ControlOptions {
vertical_strafing: garage.vertical_strafing,
sideways_driving: garage.sideways_driving,
tracks_turn_on_spot: garage.tracks_turn_on_spot,
},
mastery_level: garage.mastery_level as i32,
bay_skin_id: garage.bay_skin_id,
weapon_order: rc_database::schema::parse_int_csv(&garage.weapon_order).into_iter().map(|x| x as i32).collect(),
}
}
fn movement_category_into_data(mov_cat: &str) -> Vec<crate::data::weapon_list::ItemCategory> {
rc_database::schema::parse_int_csv(mov_cat)
.into_iter()
.filter_map(|num| crate::data::weapon_list::ItemCategory::from_bigger(num as _))
.collect()
}
pub fn i64_split(num: i64) -> (u32, u32) {
let bytes = (num as u64).to_le_bytes();
(
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]])
)
}
#[allow(dead_code)]
pub fn u64_join(uuid: (u32, u32)) -> u64 {
let bytes = (uuid.0.to_le_bytes(), uuid.1.to_le_bytes());
u64::from_le_bytes(
[bytes.0[0], bytes.0[1], bytes.0[2], bytes.0[3],
bytes.1[0], bytes.1[1], bytes.1[2], bytes.1[3]]
)
}
pub fn control_ty_into_data(control_ty: rc_database::schema::garage::ControlType) -> crate::data::garage_bay::ControlType {
match control_ty {
rc_database::schema::garage::ControlType::Camera => crate::data::garage_bay::ControlType::Camera,
rc_database::schema::garage::ControlType::Keyboard => crate::data::garage_bay::ControlType::Keyboard,
rc_database::schema::garage::ControlType::Count => crate::data::garage_bay::ControlType::Count,
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Default)]
pub enum ControlType {
#[default]

View File

@@ -6,6 +6,8 @@ pub struct Settings {
pub gameplay: super::GameplaySettings,
#[serde(default = "default_dev_messages")]
pub banners: Vec<BannerMessage>,
#[serde(default = "default_server_conf")]
pub server: ServerSettings,
}
fn default_gameplay_settings() -> super::GameplaySettings {
@@ -32,3 +34,19 @@ pub struct BannerMessage {
fn default_dev_messages() -> Vec<BannerMessage> {
Vec::default()
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerSettings {
#[serde(default = "default_db_conn")]
pub database: String,
}
fn default_db_conn() -> String {
"sqlite:../data/robocraft/accounts.sqlite.db?mode=rwc".to_owned()
}
fn default_server_conf() -> ServerSettings {
ServerSettings {
database: default_db_conn(),
}
}

View File

@@ -1,74 +1,81 @@
use argon2::PasswordVerifier;
use serde::{Serialize, Deserialize};
use crate::persist::config::ConfigProvider;
pub struct AccountProvider {
root: std::path::PathBuf,
cubes: std::sync::Arc<Vec<u32>>,
secret: Vec<u8>,
db: std::sync::Arc<rc_database::Database>,
}
impl AccountProvider {
pub fn load(root: impl AsRef<std::path::Path>, cubes: &crate::persist::config::ConfigImpl) -> std::io::Result<Self> {
pub async fn load(root: impl AsRef<std::path::Path>, conf: &crate::persist::config::ConfigImpl) -> std::io::Result<Self> {
let token_path = root.as_ref().join(super::TOKEN_SECRET_FILENAME);
let database_uri = <crate::persist::config::ConfigImpl as ConfigProvider<()>>::server_config(conf).database;
log::debug!("Connecting to user database URI: {}", database_uri);
let db = rc_database::Database::init(&database_uri).await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::NotConnected, e))?;
let root = root.as_ref().join(super::USERS_DIR);
std::fs::create_dir_all(&root)?;
Ok(Self {
root,
cubes: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::ids(cubes)),
secret: std::fs::read(&token_path)?,
})
}
pub fn load_for_auth(root: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let token_path = root.as_ref().join(super::TOKEN_SECRET_FILENAME);
let root = root.as_ref().join(super::USERS_DIR);
std::fs::create_dir_all(&root)?;
Ok(Self {
root,
cubes: std::sync::Arc::new(Vec::default()),
cubes: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::ids(conf)),
secret: std::fs::read(&token_path)?,
db: std::sync::Arc::new(db),
})
}
}
#[async_trait::async_trait]
impl <C: Clone> super::UserProvider<C> for AccountProvider {
fn authenticate(&self, token: super::UserToken, ext: std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>) -> Result<Box<dyn super::User<C> + Send + Sync>, String> {
let new_root = self.root.join(&token.uuid);
async fn authenticate(&self, token: super::UserToken, ext: std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>) -> Result<Box<dyn super::User<C> + Send + Sync>, String> {
//let new_root = self.root.join(&token.uuid);
let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret);
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_required_spec_claims::<&str>(&[]);
jsonwebtoken::decode::<libfj::robocraft::TokenPayload>(&token.token, &secret, &validation).map_err(|e| e.to_string())?;
let account_info = AccountInfo::load(&new_root).map_err(|e| e.to_string())?;
let user_info = if let Some(user_info) = self.db.user_by_public_id(token.uuid.clone()).await.map_err(|e| e.to_string())? {
user_info
} else {
return Err("User not found".to_owned());
};
let user_perms = if let Some(user_perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| e.to_string())? {
user_perms
} else {
return Err("User permissions not found".to_owned());
};
//let account_info = AccountInfo::load(&new_root).map_err(|e| e.to_string())?;
Ok(Box::new(UserData {
root: new_root,
token,
account: account_info,
account: user_info,
perms: user_perms,
cubes: self.cubes.clone(),
extensions: ext,
db: self.db.clone(),
}))
//Err("Unable to authenticate".to_string())
}
}
#[async_trait::async_trait]
impl super::UserAuthenticator for AccountProvider {
fn login(&self, info: super::UserInfo) -> Result<super::UserLoginInfo, String> {
let new_root = self.root.join(&info.payload.public_id);
let is_new_user = !new_root.exists();
if is_new_user {
std::fs::create_dir(&new_root).map_err(|e| e.to_string())?;
async fn login(&self, info: super::UserInfo) -> Result<super::UserLoginInfo, String> {
//let new_root = self.root.join(&info.payload.public_id);
let is_new_user;
let mut user_info = if let Some(user_info) = self.db.user_by_public_id(info.payload.public_id.clone()).await.map_err(|e| e.to_string())? {
is_new_user = false;
user_info
} else {
is_new_user = true;
log::info!("New user {}", info.payload.public_id);
super::setup_directory(&new_root).map_err(|e| e.to_string())?;
}
let mut account_info = AccountInfo::load(&new_root).map_err(|e| e.to_string())?;
let is_new_user = is_new_user || (account_info.password.is_none() && account_info.steam_id.is_none()); // migration
super::setup_new_user(&info, &self.db).await.map_err(|e| e.to_string())?;
self.db.user_by_public_id(info.payload.public_id.clone()).await.map_err(|e| e.to_string())?.unwrap()
};
let override_password = user_info.password.is_empty() && user_info.steam_id.is_none();
match info.extra {
super::ExtraUserInfo::Steam { id } => {
if is_new_user {
account_info.steam_id = Some(id);
}
if let Some(expected_steam_id) = account_info.steam_id {
if expected_steam_id != id {
let id_str = id.to_string();
if let Some(expected_steam_id) = user_info.steam_id {
if expected_steam_id != id_str {
return Err("SteamID does not match".to_owned())
}
} else {
@@ -78,13 +85,13 @@ impl super::UserAuthenticator for AccountProvider {
super::ExtraUserInfo::Standalone { password } => {
use argon2::password_hash::PasswordHasher;
let argon2_algo = argon2::Argon2::default();
if is_new_user {
if override_password {
let salt = argon2::password_hash::SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
let password_hash = argon2_algo.hash_password(password.as_bytes(), &salt).map_err(|e| e.to_string())?.to_string();
account_info.password = Some(password_hash);
user_info.password = password_hash;
}
if let Some(expected_password) = &account_info.password {
let expected = argon2::password_hash::PasswordHash::new(expected_password).map_err(|e| e.to_string())?;
if !user_info.password.is_empty() {
let expected = argon2::password_hash::PasswordHash::new(&user_info.password).map_err(|e| e.to_string())?;
argon2_algo.verify_password(password.as_bytes(), &expected).map_err(|e| e.to_string())?;
} else {
return Err("Password not supported for this user".to_owned())
@@ -92,9 +99,6 @@ impl super::UserAuthenticator for AccountProvider {
}
}
// authentication has now definitely succeeded
if is_new_user {
account_info.save(new_root).map_err(|e| e.to_string())?;
}
// build token
let header = jsonwebtoken::Header {
typ: Some("JWT".to_string()),
@@ -121,45 +125,35 @@ impl super::UserAuthenticator for AccountProvider {
#[allow(dead_code)]
struct UserData {
root: std::path::PathBuf,
token: super::UserToken,
account: AccountInfo,
account: rc_database::schema::user::Model,
perms: rc_database::schema::permissions::Model,
cubes: std::sync::Arc<Vec<u32>>,
extensions: std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>,
db: std::sync::Arc<rc_database::Database>,
}
impl UserData {
fn load_garage_by_id(&self, id: u32) -> std::io::Result<crate::persist::GarageSlot> {
let path = self.root.join(super::GARAGE_DIR).join(format!("{}.json", id));
crate::persist::GarageSlot::load(&path)
async fn load_garage_by_slot(&self, slot: u32) -> Result<Option<rc_database::schema::garage::Model>, rc_database::sea_orm::DbErr> {
//let path = self.root.join(super::GARAGE_DIR).join(format!("{}.json", id));
//crate::persist::GarageSlot::load(&path)
self.db.garage_by_user_id_and_slot(self.account.id, slot).await
}
fn save_garage(&self, slot: &crate::persist::GarageSlot) -> std::io::Result<()> {
let path = self.root.join(super::GARAGE_DIR).join(format!("{}.json", slot.slot));
slot.save(path)
async fn save_garage_by_slot(&self, data: rc_database::schema::garage::ActiveModel, slot: u32) -> Result<(), rc_database::sea_orm::DbErr> {
self.db.update_garage_by_user_id_and_slot(data, self.account.id, slot).await?;
Ok(())
}
fn all_vehicles(&self) -> std::io::Result<Vec<crate::persist::GarageSlot>> {
let path = self.root.join(super::GARAGE_DIR);
let mut slots = Vec::new();
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let filepath = entry.path();
if filepath.is_file() {
let slot = crate::persist::GarageSlot::load(&filepath)?;
slots.push(slot);
} else {
log::warn!("Ignoring non-file {} in {} dir", filepath.display(), super::GARAGE_DIR);
}
}
slots.sort_by_key(|slot| slot.slot);
Ok(slots)
async fn all_vehicles(&self) -> Result<Vec<rc_database::schema::garage::Model>, rc_database::sea_orm::DbErr> {
self.db.garages_by_user_id(self.account.id).await
}
}
const INVALID_ROBOT_ERR: i16 = 140;
const DATABASE_ERR: i16 = 8;
const INVALID_ROBOT_ERR: i16 = crate::data::error_codes::WebServicesError::InvalidRobot as i16; // 140
const DATABASE_ERR: i16 = crate::data::error_codes::WebServicesError::DatabaseError as i16; // 8
#[async_trait::async_trait]
impl <C: Clone> super::User<C> for UserData {
fn ext(&self, ty: std::any::TypeId) -> Option<&'_ (dyn std::any::Any + Send + Sync + 'static)> {
self.extensions.get(&ty).map(|x| x.as_ref())
@@ -170,35 +164,58 @@ impl <C: Clone> super::User<C> for UserData {
}
fn is_mod(&self) -> bool {
self.account.is_mod
self.perms.moderator
}
fn is_admin(&self) -> bool {
self.account.is_admin
self.perms.administrator
}
fn is_dev(&self) -> bool {
self.account.is_dev
self.perms.developer
}
fn unlocked_parts(&self) -> Vec<u32> {
match self.account.inventory.override_ {
super::inventory::UnlockOverride::Normal => self.account.inventory.unlocked.clone(),
super::inventory::UnlockOverride::UnlockNone => Vec::default(),
super::inventory::UnlockOverride::UnlockAll => self.cubes.as_ref().to_owned(),
async fn unlocked_parts(&self) -> Vec<u32> {
match self.db.user_aux_by_user_id_and_descriptor(self.account.id, rc_database::schema::user_aux::Descriptor::UnlockedParts).await {
Ok(Some(parts)) => {
match serde_json::from_str::<super::inventory::UnlockedParts>(&parts.data) {
Ok(json) => {
match json.override_ {
super::inventory::UnlockOverride::Normal => json.unlocked.clone(),
super::inventory::UnlockOverride::UnlockNone => Vec::default(),
super::inventory::UnlockOverride::UnlockAll => self.cubes.as_ref().to_owned(),
}
},
Err(e) => {
log::error!("Failed to deserialize Descriptor::UnlockedParts for user_id {}: {}", self.account.id, e);
Vec::default()
}
}
},
Ok(None) => Vec::default(),
Err(e) => {
log::error!("Failed to retrieve Descriptor::UnlockedParts for user_id {}: {}", self.account.id, e);
Vec::default()
}
}
}
fn selected_garage_uuid(&self) -> String {
self.account.garage.uuid_str()
async fn selected_garage(&self) -> (String, u32) {
match self.db.garage_selected(self.account.id).await {
Ok(Some(selected)) => (super::i64_as_uuid_str(selected.uuid), selected.slot),
Ok(None) => {
log::warn!("User {} does not have a selected garage", self.account.id);
("0_0".to_owned(), 0)
},
Err(e) => {
log::error!("Failed to retrieve selected garage for user_id {}: {}", self.account.id, e);
("0_0".to_owned(), 0)
}
}
}
fn selected_garage_slot(&self) -> u32 {
self.account.garage.slot
}
fn all_slots_by_id(&self) -> super::UserSlots<C> {
let slots = match self.all_vehicles() {
async fn all_slots_by_id(&self) -> super::UserSlots<C> {
let slots = match self.all_vehicles().await {
Ok(slots) => slots,
Err(e) => {
log::error!("Failed to load all vehicles: {}", e);
@@ -211,7 +228,7 @@ impl <C: Clone> super::User<C> for UserData {
val_ty: polariton::serdes:: TypePrefix::HashMap,
items: slots.into_iter().map(|slot| {
let slot_index = slot.slot;
let garage_data: crate::data::garage_bay::GarageSlotInfo = slot.into();
let garage_data: crate::data::garage_bay::GarageSlotInfo = crate::persist::garage::db_into_data(slot);
(polariton::operation::Typed::Int(slot_index as _), garage_data.as_transmissible())
}).collect(),
});
@@ -220,77 +237,67 @@ impl <C: Clone> super::User<C> for UserData {
}
}
fn slot_by_id(&self, id: i32) -> Result<crate::persist::user::UserSlotData<C>, i16> {
match self.load_garage_by_id(id as _) {
Ok(slot) => {
let control_ty: crate::data::garage_bay::ControlType = slot.control_type.into();
let control_options: crate::data::garage_bay::ControlOptions = slot.control_options.into();
async fn slot_by_id(&self, id: i32) -> Result<crate::persist::user::UserSlotData<C>, i16> {
match self.load_garage_by_slot(id as _).await {
Ok(Some(slot)) => {
let cube_count = slot.cube_count() as i32;
let control_ty: crate::data::garage_bay::ControlType = crate::persist::garage::control_ty_into_data(slot.control_type);
let control_options = crate::data::garage_bay::ControlOptions {
vertical_strafing: slot.vertical_strafing,
sideways_driving: slot.sideways_driving,
tracks_turn_on_spot: slot.tracks_turn_on_spot,
};
Ok(crate::persist::user::UserSlotData {
data: polariton::operation::Typed::Bytes(slot.robot_data.into()),
colour_data: polariton::operation::Typed::Bytes(slot.colour_data.into()),
cube_count: polariton::operation::Typed::Int(slot.cubes as _),
weapon_order: polariton::operation::Typed::IntArr(slot.weapon_order.clone().into()),
movement_categories: polariton::operation::Typed::IntArr(slot.movement_categories.into_iter().map(|cat| {
let cat: crate::data::weapon_list::ItemCategory = cat.into();
cat.but_bigger()
}).collect::<Vec<_>>().into()),
cube_count: polariton::operation::Typed::Int(cube_count),
weapon_order: polariton::operation::Typed::IntArr(rc_database::schema::parse_int_csv(&slot.weapon_order).into_iter().map(|x| x as i32).collect::<Vec<_>>().into()),
movement_categories: polariton::operation::Typed::IntArr(rc_database::schema::parse_int_csv(&slot.movement_categories).into_iter().map(|x| x as i32).collect::<Vec<_>>().into()),
control_type: polariton::operation::Typed::Int(control_ty as _),
control_options: control_options.as_transmissible(),
mastery_level: polariton::operation::Typed::Int(0), // TODO
mastery_level: polariton::operation::Typed::Int(slot.mastery_level as i32),
robot_rank: polariton::operation::Typed::Int(slot.total_robot_ranking as _),
cpu: polariton::operation::Typed::Int(slot.total_robot_cpu as _),
cosmetic_cpu: polariton::operation::Typed::Int(slot.total_cosmetic_cpu as _),
uuid: polariton::operation::Typed::Str(format!("{}_{}", slot.uuid.0, slot.uuid.1).into()),
uuid: polariton::operation::Typed::Str(super::i64_as_uuid_str(slot.uuid).into()),
})
},
Ok(None) => {
log::error!("Failed to find vehicle slot {} for user_id {}", id, self.account.id);
Err(DATABASE_ERR)
}
Err(e) => {
log::error!("Failed to load vehicle {}: {}", id, e);
log::error!("Failed to retrieve vehicle {} for user_id {}: {}", id, self.account.id, e);
Err(INVALID_ROBOT_ERR)
}
}
}
fn save_slot(&self, vehicle: crate::persist::user::VehicleData) -> Result<(), i16> {
let id = vehicle.id as u32;
let mut existing_data = self.load_garage_by_id(id).map_err(|e| {
log::error!("Failed to load vehicle {}: {}", id, e);
INVALID_ROBOT_ERR
})?;
existing_data.slot = id;
existing_data.robot_data = vehicle.robot_data;
existing_data.colour_data = vehicle.colour_data;
log::debug!("weapon order: {:?}", vehicle.weapon_order);
existing_data.weapon_order = vehicle.weapon_order;
self.save_garage(&existing_data).map_err(|e| {
log::error!("Failed to save vehicle {}: {}", id, e);
async fn save_slot(&self, vehicle: crate::persist::user::VehicleData) -> Result<(), i16> {
let entity = rc_database::schema::garage::ActiveModel {
robot_data: rc_database::sea_orm::ActiveValue::Set(vehicle.robot_data),
colour_data: rc_database::sea_orm::ActiveValue::Set(vehicle.colour_data),
weapon_order: rc_database::sea_orm::ActiveValue::Set(rc_database::schema::dump_csv(&vehicle.weapon_order)),
..Default::default()
};
self.save_garage_by_slot(entity, vehicle.slot as u32).await.map_err(|e| {
log::error!("Failed to save vehicle slot {} for user_id {}: {}", vehicle.slot, self.account.id, e);
DATABASE_ERR
})?;
Ok(())
}
fn signup_date(&self) -> i64 {
match self.root.metadata() {
Ok(meta) => {
match meta.created() {
Ok(created) => {
match created.duration_since(std::time::SystemTime::UNIX_EPOCH) {
Ok(dur) => {
return super::since_windows_epoch(dur.as_secs() as i64);
},
Err(e) => log::error!("could not get duration since unix epoch of {}: {}", self.root.display(), e),
}
},
Err(e) => log::error!("could not read creation time of {}: {}", self.root.display(), e),
}
},
Err(e) => log::error!("could not retrieve metadata of {}: {}", self.root.display(), e),
}
super::since_windows_epoch(0)
super::since_windows_epoch(self.account.creation_time)
}
fn singleplayer_robots(&self) -> Result<polariton::operation::Typed<C>, i16> {
let current_slot = self.load_garage_by_id(self.account.garage.slot).map_err(|e| {
log::error!("Failed to load current vehicle: {}", e);
async fn singleplayer_robots(&self) -> Result<polariton::operation::Typed<C>, i16> {
let current_slot = self.db.garage_selected(self.account.id).await.map_err(|e| {
log::error!("Failed to retrieve selected vehicle for user_id {} (singleplayer_robots): {}", self.account.id, e);
DATABASE_ERR
})?
.ok_or_else(|| {
log::error!("Failed to find selected vehicle for user_id {} (singleplayer_robots)", self.account.id);
INVALID_ROBOT_ERR
})?;
let user_uuid = self.token.uuid.clone();
@@ -298,52 +305,24 @@ impl <C: Clone> super::User<C> for UserData {
players: vec![
crate::data::player_data::PlayerData {
name: user_uuid.clone(),
display_name: user_uuid,
mastery: current_slot.mastery_level,
display_name: self.account.display_name.clone(),
mastery: current_slot.mastery_level as i32,
tier: 1, // FIXME
robot_name: current_slot.name,
robot_map: current_slot.robot_data,
team: 0,
has_premium: true, // FIXME
robot_uuid: format!("{}_{}", current_slot.uuid.0, current_slot.uuid.1),
robot_uuid: super::i64_as_uuid_str(current_slot.uuid).into(),
cpu: current_slot.total_robot_cpu as i32,
weapon_order: current_slot.weapon_order.clone(),
weapon_order: rc_database::schema::parse_int_csv(&current_slot.weapon_order).into_iter().map(|x| x as i32).collect::<Vec<_>>(),
colour_map: current_slot.colour_data,
is_ai: false,
spawn_effect: "Spawn_Warp".to_owned(), // FIXME
death_effect: "Explosion_Warp".to_owned(), // FIXME
player_rank: 1, // FIXME
weapon_rank: current_slot.weapon_order.into_iter().map(|x| (x, if x == 0 { 0 } else { 1 })).collect(),
weapon_rank: rc_database::schema::parse_int_csv(&current_slot.weapon_order).into_iter().map(|x| (x as i32, if x == 0 { 0 } else { 1 })).collect(),
}
],
}.as_transmissible())
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AccountInfo {
pub is_mod: bool,
pub is_admin: bool,
pub is_dev: bool,
pub password: Option<String>,
pub steam_id: Option<u64>,
pub inventory: super::UnlockedParts,
pub garage: super::SelectedGarage,
}
impl AccountInfo {
fn load(root: impl AsRef<std::path::Path>) -> std::io::Result<AccountInfo> {
let file = std::fs::File::open(root.as_ref().join(super::USER_FILE))?;
let buffered = std::io::BufReader::new(file);
let result = serde_json::from_reader(buffered)?;
Ok(result)
}
pub fn save(&self, root: impl AsRef<std::path::Path>) -> std::io::Result<()> {
let file = std::fs::File::create(root.as_ref().join(super::USER_FILE))?;
let buffered = std::io::BufWriter::new(file);
serde_json::to_writer_pretty(buffered, self)?;
Ok(())
}
}

View File

@@ -1,30 +1,68 @@
const REFERENCE_DIR: &str = "layout folder";
//const REFERENCE_DIR: &str = "layout folder";
fn build_reference_directory(root: impl AsRef<std::path::Path>) -> std::io::Result<()> {
std::fs::create_dir(&root)?;
let garage_dir = root.as_ref().join(super::GARAGE_DIR);
std::fs::create_dir(&garage_dir)?;
default_user_data().save(&root)?;
for slot in default_garage_slots() {
fn current_unix_time() -> i64 {
chrono::Utc::now().timestamp()
}
async fn build_new_account_data(user: &super::UserInfo, db: &rc_database::Database) -> Result<(), rc_database::sea_orm::DbErr> {
//std::fs::create_dir(&root)?;
//let garage_dir = root.as_ref().join(super::GARAGE_DIR);
//std::fs::create_dir(&garage_dir)?;
let user_data = db.insert_user(default_user_data(user)).await?;
db.insert_perms(default_user_perms(user_data.id)).await?;
db.insert_user_aux(default_user_aux_data(user_data.id)).await?;
db.insert_garages(default_garage_slots(user_data.id)).await?;
/*for slot in default_garage_slots() {
let filepath = garage_dir.join(format!("{}.json", slot.slot));
slot.save(filepath)?;
}
}*/
Ok(())
}
pub fn setup_directory(new_dir: impl AsRef<std::path::Path>) -> std::io::Result<()> {
let ref_path = new_dir.as_ref().parent().unwrap().join(REFERENCE_DIR);
pub async fn setup_new_user(user: &super::UserInfo, db: &rc_database::Database) -> Result<(), rc_database::sea_orm::DbErr> {
/*let ref_path = new_dir.as_ref().parent().unwrap().join(REFERENCE_DIR);
if !ref_path.exists() {
log::debug!("Initialising reference directory {}", ref_path.display());
build_reference_directory(&ref_path)?;
build_reference_directory(user)?;
}
log::debug!("Copying reference directory for new user: {}", new_dir.as_ref().display());
so::copy_dir_all(ref_path, new_dir)?;
so::copy_dir_all(ref_path, new_dir)?;*/
build_new_account_data(user, db).await?;
Ok(())
}
fn default_user_data() -> super::AccountInfo {
super::AccountInfo {
fn default_user_data(user: &super::UserInfo) -> rc_database::schema::user::ActiveModel {
let password = if let super::ExtraUserInfo::Standalone { password } = &user.extra {
//password.to_owned()
use argon2::password_hash::PasswordHasher;
let argon2_algo = argon2::Argon2::default();
let salt = argon2::password_hash::SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
match argon2_algo.hash_password(password.as_bytes(), &salt) {
Err(e) => {
log::error!("Failed to hash password for user {}/{}: {}", user.payload.public_id, user.payload.display_name, e);
"".to_owned()
},
Ok(password) => password.to_string(),
}
} else {
Default::default()
};
let steam_id = if let super::ExtraUserInfo::Steam { id } = &user.extra {
Some(id.to_string())
} else {
None
};
rc_database::schema::user::ActiveModel {
id: Default::default(),
creation_time: rc_database::sea_orm::ActiveValue::Set(current_unix_time()),
public_id: rc_database::sea_orm::ActiveValue::Set(user.payload.public_id.clone()),
display_name: rc_database::sea_orm::ActiveValue::Set(user.payload.display_name.clone()),
password: rc_database::sea_orm::ActiveValue::Set(password),
email: rc_database::sea_orm::ActiveValue::Set("//TODO".to_owned()),
steam_id: rc_database::sea_orm::ActiveValue::Set(steam_id),
}
/*super::AccountInfo {
is_mod: false,
is_admin: false,
is_dev: false,
@@ -38,11 +76,112 @@ fn default_user_data() -> super::AccountInfo {
uuid: (0, 0),
slot: 0,
},
}*/
}
fn default_user_aux_data(user_id: u32) -> Vec<rc_database::schema::user_aux::ActiveModel> {
vec![
rc_database::schema::user_aux::ActiveModel {
id: Default::default(),
user_id: rc_database::sea_orm::ActiveValue::Set(user_id),
creation_time: rc_database::sea_orm::ActiveValue::Set(current_unix_time()),
descriptor: rc_database::sea_orm::ActiveValue::Set(rc_database::schema::user_aux::Descriptor::UserXP),
data: rc_database::sea_orm::ActiveValue::Set("0".to_owned()),
},
rc_database::schema::user_aux::ActiveModel {
id: Default::default(),
user_id: rc_database::sea_orm::ActiveValue::Set(user_id),
creation_time: rc_database::sea_orm::ActiveValue::Set(current_unix_time()),
descriptor: rc_database::sea_orm::ActiveValue::Set(rc_database::schema::user_aux::Descriptor::PremiumExpiry),
data: rc_database::sea_orm::ActiveValue::Set(current_unix_time().to_string()),
},
rc_database::schema::user_aux::ActiveModel {
id: Default::default(),
user_id: rc_database::sea_orm::ActiveValue::Set(user_id),
creation_time: rc_database::sea_orm::ActiveValue::Set(current_unix_time()),
descriptor: rc_database::sea_orm::ActiveValue::Set(rc_database::schema::user_aux::Descriptor::UnlockedParts),
data: rc_database::sea_orm::ActiveValue::Set(
r#"{
"unlocked": [],
"override": "UnlockAll"
}"#.to_owned()),
},
rc_database::schema::user_aux::ActiveModel {
id: Default::default(),
user_id: rc_database::sea_orm::ActiveValue::Set(user_id),
creation_time: rc_database::sea_orm::ActiveValue::Set(current_unix_time()),
descriptor: rc_database::sea_orm::ActiveValue::Set(rc_database::schema::user_aux::Descriptor::TechPoints),
data: rc_database::sea_orm::ActiveValue::Set("1337".to_owned()),
},
rc_database::schema::user_aux::ActiveModel {
id: Default::default(),
user_id: rc_database::sea_orm::ActiveValue::Set(user_id),
creation_time: rc_database::sea_orm::ActiveValue::Set(current_unix_time()),
descriptor: rc_database::sea_orm::ActiveValue::Set(rc_database::schema::user_aux::Descriptor::UserRank),
data: rc_database::sea_orm::ActiveValue::Set("1".to_owned()),
},
rc_database::schema::user_aux::ActiveModel {
id: Default::default(),
user_id: rc_database::sea_orm::ActiveValue::Set(user_id),
creation_time: rc_database::sea_orm::ActiveValue::Set(current_unix_time()),
descriptor: rc_database::sea_orm::ActiveValue::Set(rc_database::schema::user_aux::Descriptor::UserFreeCurrency),
data: rc_database::sea_orm::ActiveValue::Set("10000".to_owned()),
},
rc_database::schema::user_aux::ActiveModel {
id: Default::default(),
user_id: rc_database::sea_orm::ActiveValue::Set(user_id),
creation_time: rc_database::sea_orm::ActiveValue::Set(current_unix_time()),
descriptor: rc_database::sea_orm::ActiveValue::Set(rc_database::schema::user_aux::Descriptor::UserPaidCurrency),
data: rc_database::sea_orm::ActiveValue::Set("1000".to_owned()),
}
]
}
fn default_user_perms(user_id: u32) -> rc_database::schema::permissions::ActiveModel {
rc_database::schema::permissions::ActiveModel {
id: Default::default(),
user_id: rc_database::sea_orm::ActiveValue::Set(user_id),
moderator: rc_database::sea_orm::ActiveValue::Set(false),
administrator: rc_database::sea_orm::ActiveValue::Set(false),
developer: rc_database::sea_orm::ActiveValue::Set(false),
royalty: rc_database::sea_orm::ActiveValue::Set(false),
banned: rc_database::sea_orm::ActiveValue::Set(false),
}
}
fn default_garage_slots() -> Vec<crate::persist::GarageSlot> {
fn default_garage_slots(user_id: u32) -> Vec<rc_database::schema::garage::ActiveModel> {
let current_time = current_unix_time();
vec![
rc_database::schema::garage::ActiveModel {
id: Default::default(),
user_id: rc_database::sea_orm::ActiveValue::Set(user_id),
creation_time: rc_database::sea_orm::ActiveValue::Set(current_time),
slot: rc_database::sea_orm::ActiveValue::Set(0),
name: rc_database::sea_orm::ActiveValue::Set("Bay 0".to_owned()),
crf_id: rc_database::sea_orm::ActiveValue::Set(None),
was_rated: rc_database::sea_orm::ActiveValue::Set(false),
movement_categories: rc_database::sea_orm::ActiveValue::Set("".to_owned()),
uuid: rc_database::sea_orm::ActiveValue::Set(current_time),
thumbnail_version: rc_database::sea_orm::ActiveValue::Set(0),
total_robot_cpu: rc_database::sea_orm::ActiveValue::Set(0),
total_cosmetic_cpu: rc_database::sea_orm::ActiveValue::Set(0),
total_robot_ranking: rc_database::sea_orm::ActiveValue::Set(0),
bay_cpu: rc_database::sea_orm::ActiveValue::Set(2_000),
tutorial_robot: rc_database::sea_orm::ActiveValue::Set(false),
starter_robot_index: rc_database::sea_orm::ActiveValue::Set(None),
control_type: rc_database::sea_orm::ActiveValue::Set(rc_database::schema::garage::ControlType::Camera),
vertical_strafing: rc_database::sea_orm::ActiveValue::Set(false),
sideways_driving: rc_database::sea_orm::ActiveValue::Set(false),
tracks_turn_on_spot: rc_database::sea_orm::ActiveValue::Set(false),
mastery_level: rc_database::sea_orm::ActiveValue::Set(1),
bay_skin_id: rc_database::sea_orm::ActiveValue::Set("RC_MothershipSkin_Neptune_01".to_owned()),
weapon_order: rc_database::sea_orm::ActiveValue::Set("".to_owned()),
robot_data: rc_database::sea_orm::ActiveValue::Set(vec![0u8; 4]),
colour_data: rc_database::sea_orm::ActiveValue::Set(vec![0u8; 4]),
selected: rc_database::sea_orm::ActiveValue::Set(true),
}
]
/*vec![
crate::persist::GarageSlot {
slot: 0,
name: "Bay 1".to_owned(),
@@ -66,25 +205,5 @@ fn default_garage_slots() -> Vec<crate::persist::GarageSlot> {
robot_data: vec![0; 4],
colour_data: vec![0; 4],
}
]
}
mod so {
// from https://stackoverflow.com/questions/26958489/how-to-copy-a-folder-recursively-in-rust
use std::path::Path;
use std::{io, fs};
pub(super) fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
fs::create_dir_all(&dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}
]*/
}

View File

@@ -1,11 +1,11 @@
mod account_json;
pub use account_json::{AccountProvider, AccountInfo};
pub use account_json::AccountProvider;
mod garage_data;
pub use garage_data::SelectedGarage;
mod initial_data;
pub use initial_data::setup_directory;
pub use initial_data::setup_new_user;
mod inventory;
pub use inventory::UnlockedParts;
@@ -34,3 +34,11 @@ pub fn since_windows_epoch(since_unix_epoch: i64) -> i64 {
//let time_in = chrono::Utc.from_utc_datetime(&chrono::NaiveDateTime::from_timestamp(since_unix_epoch, 0));
time_in.signed_duration_since(windows_epoch).num_milliseconds() * 10_000
}
pub fn uuid_str(uuid: &(u32, u32)) -> String {
format!("{}_{}", uuid.0, uuid.1)
}
pub fn i64_as_uuid_str(num: i64) -> String {
uuid_str(&super::garage::i64_split(num))
}

View File

@@ -25,28 +25,30 @@ pub struct UserLoginInfo {
pub is_new: bool,
}
#[async_trait::async_trait]
pub trait UserProvider<C> {
fn authenticate(&self, user: UserToken, ext: std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>) -> Result<Box<dyn User<C> + Send + Sync>, String>;
async fn authenticate(&self, user: UserToken, ext: std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>) -> Result<Box<dyn User<C> + Send + Sync>, String>;
}
#[async_trait::async_trait]
pub trait UserAuthenticator {
fn login(&self, info: UserInfo) -> Result<UserLoginInfo, String>;
async fn login(&self, info: UserInfo) -> Result<UserLoginInfo, String>;
}
#[async_trait::async_trait]
pub trait User<C> {
fn ext(&self, ty: std::any::TypeId) -> Option<&'_ (dyn std::any::Any + Send + Sync + 'static)>;
fn token(&self) -> &'_ super::UserToken;
fn is_mod(&self) -> bool;
fn is_admin(&self) -> bool;
fn is_dev(&self) -> bool;
fn unlocked_parts(&self) -> Vec<u32>;
fn selected_garage_uuid(&self) -> String;
fn selected_garage_slot(&self) -> u32;
fn all_slots_by_id(&self) -> UserSlots<C>;
fn slot_by_id(&self, id: i32) -> Result<UserSlotData<C>, i16>;
fn save_slot(&self, vehicle: VehicleData) -> Result<(), i16>;
async fn unlocked_parts(&self) -> Vec<u32>;
async fn selected_garage(&self) -> (String, u32);
async fn all_slots_by_id(&self) -> UserSlots<C>;
async fn slot_by_id(&self, id: i32) -> Result<UserSlotData<C>, i16>;
async fn save_slot(&self, vehicle: VehicleData) -> Result<(), i16>;
fn signup_date(&self) -> i64;
fn singleplayer_robots(&self) -> Result<polariton::operation::Typed<C>, i16>;
async fn singleplayer_robots(&self) -> Result<polariton::operation::Typed<C>, i16>;
}
pub struct UserSlots<C> {
@@ -70,7 +72,7 @@ pub struct UserSlotData<C> {
}
pub struct VehicleData {
pub id: i32,
pub slot: i32,
pub robot_data: Vec<u8>,
pub colour_data: Vec<u8>,
pub weapon_order: Vec<i32>,

View File

@@ -7,13 +7,14 @@ pub struct UserState<C: Clone = ()> {
}
impl <C: Clone> UserState<C> {
pub fn update_with_auth(&self, auth_str: &str) -> bool {
self.update_with_auth_ext(auth_str, |_| Some(Default::default()))
pub async fn update_with_auth(&self, auth_str: &str) -> bool {
self.update_with_auth_ext(auth_str, |_| Some(Default::default())).await
}
pub fn update_with_auth_ext<F: FnOnce(&crate::persist::user::UserToken) -> Option<std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>>>(&self, auth_str: &str, ext_f: F) -> bool {
let mut lock = self.state.write().unwrap();
match &*lock {
pub async fn update_with_auth_ext<F: FnOnce(&crate::persist::user::UserToken) -> Option<std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>>>(&self, auth_str: &str, ext_f: F) -> bool {
//let mut lock = self.state.write().unwrap();
let init_state_clone = self.state.write().unwrap().clone();
match init_state_clone {
InitState::Unauthenticated(auth) => {
let splits: Vec<&str> = auth_str.split(';').collect();
if splits.len() != 3 {
@@ -30,8 +31,9 @@ impl <C: Clone> UserState<C> {
} else {
return false;
};
match auth.authenticate(token, ext) {
match auth.authenticate(token, ext).await {
Ok(user) => {
let mut lock = self.state.write().unwrap();
*lock = InitState::Authenticated(std::sync::Arc::new(user));
true
},
@@ -74,6 +76,7 @@ impl <C: Clone> UserState<C> {
}
}
#[derive(Clone)]
enum InitState<C> {
Unauthenticated(std::sync::Arc<crate::persist::user::UserImpl>),
Authenticated(std::sync::Arc<Box<dyn crate::persist::user::User<C> + Send + Sync>>),