You've already forked AstralRinth
forked from didirus/AstralRinth
Authentication (#37)
* Initial authentication implementation * Store user info in the database, improve encapsulation in profiles * Add user list, remove unused dependencies, add spantraces * Implement user remove, update UUID crate * Add user set-default * Revert submodule macro usage * Make tracing significantly less verbose
This commit is contained in:
@@ -10,10 +10,11 @@ pub struct DirectoryInfo {
|
||||
|
||||
impl DirectoryInfo {
|
||||
/// Get all paths needed for Theseus to operate properly
|
||||
#[tracing::instrument]
|
||||
pub async fn init() -> crate::Result<Self> {
|
||||
// Working directory
|
||||
let working_dir = std::env::current_dir().map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Could not open working directory: {err}"
|
||||
))
|
||||
})?;
|
||||
@@ -21,12 +22,12 @@ impl DirectoryInfo {
|
||||
// Config directory
|
||||
let config_dir = Self::env_path("THESEUS_CONFIG_DIR")
|
||||
.or_else(|| Some(dirs::config_dir()?.join("theseus")))
|
||||
.ok_or(crate::Error::FSError(
|
||||
.ok_or(crate::ErrorKind::FSError(
|
||||
"Could not find valid config dir".to_string(),
|
||||
))?;
|
||||
|
||||
fs::create_dir_all(&config_dir).await.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Error creating Theseus config directory: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -13,6 +13,7 @@ use std::collections::LinkedList;
|
||||
const METADATA_URL: &str = "https://meta.modrinth.com/gamedata";
|
||||
const METADATA_DB_FIELD: &[u8] = b"metadata";
|
||||
|
||||
// TODO: store as subtree in database
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
pub struct Metadata {
|
||||
pub minecraft: MinecraftManifest,
|
||||
@@ -48,6 +49,7 @@ impl Metadata {
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||
let mut metadata = None;
|
||||
|
||||
@@ -84,7 +86,10 @@ impl Metadata {
|
||||
db.flush_async().await?;
|
||||
Ok(meta)
|
||||
} else {
|
||||
Err(crate::Error::NoValueFor(String::from("launcher metadata")))
|
||||
Err(
|
||||
crate::ErrorKind::NoValueFor(String::from("launcher metadata"))
|
||||
.as_error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,19 @@ mod dirs;
|
||||
pub use self::dirs::*;
|
||||
|
||||
mod metadata;
|
||||
pub use metadata::*;
|
||||
|
||||
mod settings;
|
||||
pub use settings::*;
|
||||
pub use self::metadata::*;
|
||||
|
||||
mod profiles;
|
||||
pub use profiles::*;
|
||||
pub use self::profiles::*;
|
||||
|
||||
mod settings;
|
||||
pub use self::settings::*;
|
||||
|
||||
mod users;
|
||||
pub use self::users::*;
|
||||
|
||||
// Global state
|
||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
/// Database, used to store some information
|
||||
pub(self) database: sled::Db,
|
||||
@@ -28,52 +30,62 @@ pub struct State {
|
||||
pub io_semaphore: Semaphore,
|
||||
/// Launcher metadata
|
||||
pub metadata: Metadata,
|
||||
// TODO: settings API
|
||||
/// Launcher configuration
|
||||
pub settings: RwLock<Settings>,
|
||||
/// Launcher profile metadata
|
||||
pub profiles: RwLock<Profiles>,
|
||||
pub(crate) profiles: RwLock<Profiles>,
|
||||
/// Launcher user account info
|
||||
pub(crate) users: RwLock<Users>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
#[tracing::instrument]
|
||||
/// Get the current launcher state, initializing it if needed
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
LAUNCHER_STATE
|
||||
.get_or_try_init(|| async {
|
||||
// Directories
|
||||
let directories = DirectoryInfo::init().await?;
|
||||
.get_or_try_init(|| {
|
||||
async {
|
||||
// Directories
|
||||
let directories = DirectoryInfo::init().await?;
|
||||
|
||||
// Database
|
||||
// TODO: make database versioned
|
||||
let database =
|
||||
sled_config().path(directories.database_file()).open()?;
|
||||
// Database
|
||||
// TODO: make database versioned
|
||||
let database = sled_config()
|
||||
.path(directories.database_file())
|
||||
.open()?;
|
||||
|
||||
// Settings
|
||||
let settings =
|
||||
Settings::init(&directories.settings_file()).await?;
|
||||
// Settings
|
||||
let settings =
|
||||
Settings::init(&directories.settings_file()).await?;
|
||||
|
||||
// Metadata
|
||||
let metadata = Metadata::init(&database).await?;
|
||||
// Launcher data
|
||||
let (metadata, profiles) = tokio::try_join! {
|
||||
Metadata::init(&database),
|
||||
Profiles::init(&database),
|
||||
}?;
|
||||
let users = Users::init(&database)?;
|
||||
|
||||
// Profiles
|
||||
let profiles = Profiles::init(&database).await?;
|
||||
// Loose initializations
|
||||
let io_semaphore =
|
||||
Semaphore::new(settings.max_concurrent_downloads);
|
||||
|
||||
// Loose initializations
|
||||
let io_semaphore =
|
||||
Semaphore::new(settings.max_concurrent_downloads);
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
database,
|
||||
directories,
|
||||
io_semaphore,
|
||||
metadata,
|
||||
settings: RwLock::new(settings),
|
||||
profiles: RwLock::new(profiles),
|
||||
}))
|
||||
Ok(Arc::new(Self {
|
||||
database,
|
||||
directories,
|
||||
io_semaphore,
|
||||
metadata,
|
||||
settings: RwLock::new(settings),
|
||||
profiles: RwLock::new(profiles),
|
||||
users: RwLock::new(users),
|
||||
}))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map(Arc::clone)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
/// Synchronize in-memory state with persistent state
|
||||
pub async fn sync() -> crate::Result<()> {
|
||||
let state = Self::get().await?;
|
||||
|
||||
@@ -12,8 +12,7 @@ use tokio::fs;
|
||||
const PROFILE_JSON_PATH: &str = "profile.json";
|
||||
const PROFILE_SUBTREE: &[u8] = b"profiles";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
|
||||
pub(crate) struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
|
||||
|
||||
// TODO: possibly add defaults to some of these values
|
||||
pub const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
@@ -84,15 +83,17 @@ pub struct JavaSettings {
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
#[tracing::instrument]
|
||||
pub async fn new(
|
||||
name: String,
|
||||
version: String,
|
||||
path: PathBuf,
|
||||
) -> crate::Result<Self> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(crate::Error::InputError(String::from(
|
||||
return Err(crate::ErrorKind::InputError(String::from(
|
||||
"Empty name for instance!",
|
||||
)));
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
@@ -114,11 +115,13 @@ impl Profile {
|
||||
|
||||
// TODO: deduplicate these builder methods
|
||||
// They are flat like this in order to allow builder-style usage
|
||||
#[tracing::instrument]
|
||||
pub fn with_name(&mut self, name: String) -> &mut Self {
|
||||
self.metadata.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn with_icon<'a>(
|
||||
&'a mut self,
|
||||
icon: &'a Path,
|
||||
@@ -136,17 +139,20 @@ impl Profile {
|
||||
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(crate::Error::InputError(format!(
|
||||
Err(crate::ErrorKind::InputError(format!(
|
||||
"Unsupported image type: {ext}"
|
||||
)))
|
||||
))
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_game_version(&mut self, version: String) -> &mut Self {
|
||||
self.metadata.game_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_loader(
|
||||
&mut self,
|
||||
loader: ModLoader,
|
||||
@@ -157,6 +163,7 @@ impl Profile {
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_java_settings(
|
||||
&mut self,
|
||||
settings: Option<JavaSettings>,
|
||||
@@ -165,6 +172,7 @@ impl Profile {
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_memory(
|
||||
&mut self,
|
||||
settings: Option<MemorySettings>,
|
||||
@@ -173,6 +181,7 @@ impl Profile {
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_resolution(
|
||||
&mut self,
|
||||
resolution: Option<WindowSize>,
|
||||
@@ -181,6 +190,7 @@ impl Profile {
|
||||
self
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn with_hooks(&mut self, hooks: Option<Hooks>) -> &mut Self {
|
||||
self.hooks = hooks;
|
||||
self
|
||||
@@ -188,6 +198,7 @@ impl Profile {
|
||||
}
|
||||
|
||||
impl Profiles {
|
||||
#[tracing::instrument(skip(db))]
|
||||
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
|
||||
Ok(Default::default()),
|
||||
@@ -218,19 +229,23 @@ impl Profiles {
|
||||
Ok(Self(profiles))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
||||
self.0.insert(
|
||||
profile
|
||||
.path
|
||||
.canonicalize()?
|
||||
.to_str()
|
||||
.ok_or(crate::Error::UTFError(profile.path.clone()))?
|
||||
.ok_or(
|
||||
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
|
||||
)?
|
||||
.into(),
|
||||
Some(profile),
|
||||
);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn insert_from<'a>(
|
||||
&'a mut self,
|
||||
path: &'a Path,
|
||||
@@ -238,12 +253,14 @@ impl Profiles {
|
||||
self.insert(Self::read_profile_from_dir(&path.canonicalize()?).await?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
|
||||
let path = PathBuf::from(path.canonicalize()?.to_str().unwrap());
|
||||
self.0.remove(&path);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn sync<'a>(
|
||||
&'a self,
|
||||
batch: &'a mut sled::Batch,
|
||||
|
||||
@@ -19,6 +19,7 @@ pub struct Settings {
|
||||
pub custom_java_args: Vec<String>,
|
||||
pub java_8_path: Option<PathBuf>,
|
||||
pub java_17_path: Option<PathBuf>,
|
||||
pub default_user: Option<uuid::Uuid>,
|
||||
pub hooks: Hooks,
|
||||
pub max_concurrent_downloads: usize,
|
||||
pub version: u32,
|
||||
@@ -32,6 +33,7 @@ impl Default for Settings {
|
||||
custom_java_args: Vec::new(),
|
||||
java_8_path: None,
|
||||
java_17_path: None,
|
||||
default_user: None,
|
||||
hooks: Hooks::default(),
|
||||
max_concurrent_downloads: 64,
|
||||
version: CURRENT_FORMAT_VERSION,
|
||||
@@ -40,14 +42,16 @@ impl Default for Settings {
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
#[tracing::instrument]
|
||||
pub async fn init(file: &Path) -> crate::Result<Self> {
|
||||
if file.exists() {
|
||||
fs::read(&file)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Error reading settings file: {err}"
|
||||
))
|
||||
.as_error()
|
||||
})
|
||||
.and_then(|it| {
|
||||
serde_json::from_slice::<Settings>(&it)
|
||||
@@ -58,13 +62,15 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
|
||||
fs::write(to, serde_json::to_vec_pretty(self)?)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Error saving settings to file: {err}"
|
||||
))
|
||||
.as_error()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
79
theseus/src/state/users.rs
Normal file
79
theseus/src/state/users.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
//! User login info
|
||||
use crate::{auth::Credentials, config::BINCODE_CONFIG};
|
||||
|
||||
const USER_DB_TREE: &[u8] = b"users";
|
||||
|
||||
/// The set of users stored in the launcher
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Users(pub(crate) sled::Tree);
|
||||
|
||||
impl Users {
|
||||
#[tracing::instrument(skip(db))]
|
||||
pub fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||
Ok(Self(db.open_tree(USER_DB_TREE)?))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
credentials: &Credentials,
|
||||
) -> crate::Result<&Self> {
|
||||
let id = credentials.id.as_bytes();
|
||||
self.0.insert(
|
||||
id,
|
||||
bincode::encode_to_vec(credentials, *BINCODE_CONFIG)?,
|
||||
)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn contains(&self, id: uuid::Uuid) -> crate::Result<bool> {
|
||||
Ok(self.0.contains_key(id.as_bytes())?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn get(&self, id: uuid::Uuid) -> crate::Result<Option<Credentials>> {
|
||||
self.0.get(id.as_bytes())?.map_or(Ok(None), |prof| {
|
||||
bincode::decode_from_slice(&prof, *BINCODE_CONFIG)
|
||||
.map_err(crate::Error::from)
|
||||
.map(|it| Some(it.0))
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn remove(&mut self, id: uuid::Uuid) -> crate::Result<&Self> {
|
||||
self.0.remove(id.as_bytes())?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> UserIter<impl UserInnerIter> {
|
||||
UserIter(self.0.iter().values(), false)
|
||||
}
|
||||
}
|
||||
|
||||
alias_trait! {
|
||||
pub UserInnerIter: Iterator<Item = sled::Result<sled::IVec>>, Send, Sync
|
||||
}
|
||||
|
||||
/// An iterator over the set of users
|
||||
#[derive(Debug)]
|
||||
pub struct UserIter<I: UserInnerIter>(I, bool);
|
||||
|
||||
impl<I: UserInnerIter> Iterator for UserIter<I> {
|
||||
type Item = crate::Result<Credentials>;
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let it = self.0.next()?;
|
||||
let res = it.map_err(crate::Error::from).and_then(|it| {
|
||||
Ok(bincode::decode_from_slice(&it, *BINCODE_CONFIG)?.0)
|
||||
});
|
||||
|
||||
self.1 = res.is_err();
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user