pub mod auth;
pub mod uuid;
use std::{collections::HashMap, ops::Deref, sync::Arc};
use anyhow::bail;
use mcvm_auth::mc::{AccessToken, ClientId, Keypair};
use mcvm_shared::output::MCVMOutput;
use reqwest::Client;
use crate::{net::minecraft::MinecraftUserProfile, Paths};
use self::auth::AuthParameters;
pub type UserID = Arc<str>;
#[derive(Debug, Clone)]
pub struct User {
pub(crate) kind: UserKind,
id: UserID,
name: Option<String>,
uuid: Option<String>,
access_token: Option<AccessToken>,
keypair: Option<Keypair>,
}
#[derive(Debug, Clone)]
pub enum UserKind {
Microsoft {
xbox_uid: Option<String>,
},
Demo,
Unknown(String),
}
impl User {
pub fn new(kind: UserKind, id: UserID) -> Self {
Self {
kind,
id,
name: None,
uuid: None,
access_token: None,
keypair: None,
}
}
pub fn get_id(&self) -> &UserID {
&self.id
}
pub fn get_name(&self) -> Option<&String> {
self.name.as_ref()
}
pub fn is_microsoft(&self) -> bool {
matches!(self.kind, UserKind::Microsoft { .. })
}
pub fn is_demo(&self) -> bool {
matches!(self.kind, UserKind::Demo)
}
pub fn get_kind(&self) -> &UserKind {
&self.kind
}
pub fn set_uuid(&mut self, uuid: &str) {
self.uuid = Some(uuid.to_string());
}
pub fn get_uuid(&self) -> Option<&String> {
self.uuid.as_ref()
}
pub fn get_access_token(&self) -> Option<&AccessToken> {
self.access_token.as_ref()
}
pub fn get_xbox_uid(&self) -> Option<&String> {
if let UserKind::Microsoft { xbox_uid } = &self.kind {
xbox_uid.as_ref()
} else {
None
}
}
pub fn get_keypair(&self) -> Option<&Keypair> {
self.keypair.as_ref()
}
pub fn validate_username(&self) -> bool {
if let Some(name) = &self.name {
if name.is_empty() || name.len() > 16 {
return false;
}
for c in name.chars() {
if !c.is_ascii_alphanumeric() && c != '_' {
return false;
}
}
}
true
}
}
#[derive(Clone)]
pub struct UserManager {
state: AuthState,
users: HashMap<UserID, User>,
ms_client_id: ClientId,
offline: bool,
custom_auth_fn: Option<CustomAuthFunction>,
}
#[derive(Debug, Clone)]
enum AuthState {
Offline,
UserChosen(UserID),
}
impl UserManager {
pub fn new(ms_client_id: ClientId) -> Self {
Self {
state: AuthState::Offline,
users: HashMap::new(),
ms_client_id,
offline: false,
custom_auth_fn: None,
}
}
pub fn add_user(&mut self, user: User) {
self.add_user_with_id(user.id.clone(), user);
}
pub fn add_user_with_id(&mut self, user_id: UserID, user: User) {
self.users.insert(user_id, user);
}
pub fn get_user(&self, user_id: &str) -> Option<&User> {
self.users.get(user_id)
}
pub fn get_user_mut(&mut self, user_id: &str) -> Option<&mut User> {
self.users.get_mut(user_id)
}
pub fn user_exists(&self, user_id: &str) -> bool {
self.users.contains_key(user_id)
}
pub fn iter_users(&self) -> impl Iterator<Item = (&UserID, &User)> {
self.users.iter()
}
pub fn remove_user(&mut self, user_id: &str) {
let is_chosen = if let Some(chosen) = self.get_chosen_user() {
chosen.get_id().deref() == user_id
} else {
false
};
if is_chosen {
self.unchoose_user();
}
self.users.remove(user_id);
}
pub fn choose_user(&mut self, user_id: &str) -> anyhow::Result<()> {
if !self.user_exists(user_id) {
bail!("Chosen user does not exist");
}
self.state = AuthState::UserChosen(user_id.into());
Ok(())
}
pub fn get_chosen_user(&self) -> Option<&User> {
match &self.state {
AuthState::Offline => None,
AuthState::UserChosen(user_id) => self.users.get(user_id),
}
}
pub fn get_chosen_user_mut(&mut self) -> Option<&mut User> {
match &self.state {
AuthState::Offline => None,
AuthState::UserChosen(user_id) => self.users.get_mut(user_id),
}
}
pub fn is_user_chosen(&self) -> bool {
matches!(self.state, AuthState::UserChosen(..))
}
pub fn is_authenticated(&self) -> bool {
let Some(user) = self.get_chosen_user() else {
return false;
};
user.is_authenticated()
}
pub async fn authenticate(
&mut self,
paths: &Paths,
client: &Client,
o: &mut impl MCVMOutput,
) -> anyhow::Result<()> {
if let AuthState::UserChosen(user_id) = &mut self.state {
let user = self
.users
.get_mut(user_id)
.expect("User in AuthState does not exist");
if !user.is_authenticated() || !user.is_auth_valid(paths) {
let params = AuthParameters {
req_client: client,
paths,
force: false,
offline: self.offline,
client_id: self.ms_client_id.clone(),
custom_auth_fn: self.custom_auth_fn.clone(),
};
user.authenticate(params, o).await?;
}
}
Ok(())
}
pub fn unchoose_user(&mut self) {
self.state = AuthState::Offline;
}
pub fn steal_users(&mut self, other: &Self) {
self.users.extend(other.users.clone());
self.state = other.state.clone();
}
pub fn set_offline(&mut self, offline: bool) {
self.offline = offline;
}
pub fn set_custom_auth_function(&mut self, func: CustomAuthFunction) {
self.custom_auth_fn = Some(func);
}
}
pub type CustomAuthFunction =
Arc<dyn Fn(&str, &str) -> anyhow::Result<Option<MinecraftUserProfile>> + Send + Sync>;
pub fn validate_username(_kind: &UserKind, name: &str) -> bool {
if name.is_empty() || name.len() > 16 {
return false;
}
for c in name.chars() {
if !c.is_ascii_alphanumeric() && c != '_' {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_username_validation() {
assert!(validate_username(
&UserKind::Microsoft { xbox_uid: None },
"CarbonSmasher"
));
assert!(validate_username(&UserKind::Demo, "12345"));
assert!(validate_username(
&UserKind::Microsoft { xbox_uid: None },
"Foo_Bar888"
));
assert!(!validate_username(
&UserKind::Microsoft { xbox_uid: None },
""
));
assert!(!validate_username(
&UserKind::Microsoft { xbox_uid: None },
"ABCDEFGHIJKLMNOPQRS"
));
assert!(!validate_username(
&UserKind::Microsoft { xbox_uid: None },
"+++"
));
}
#[test]
fn test_user_manager() {
let mut users = UserManager::new(ClientId::new(String::new()));
let user = User::new(UserKind::Demo, "foo".into());
users.add_user(user);
users.choose_user("foo").expect("Failed to choose user");
let user = User::new(UserKind::Demo, "bar".into());
users.add_user(user);
users.remove_user("foo");
assert!(!users.is_user_chosen());
assert!(!users.user_exists("foo"));
}
}