feat: Account page fixes + ready to integrate with auth

This commit is contained in:
LynithDev 2024-07-28 20:24:19 +02:00
parent 05e6db5b1d
commit 4539cb5388
10 changed files with 266 additions and 89 deletions

View file

@ -6,8 +6,7 @@ fn main() {
"onelauncher",
tauri_build::InlinedPlugin::new().commands(&[
// User
"begin_msa",
"finish_msa",
"msa_auth",
"get_users",
"get_user",
"remove_user",

View file

@ -2,8 +2,7 @@
description = "Default permissions for the plugin"
permissions = [
# User
"allow-begin-msa",
"allow-finish-msa",
"allow-msa-auth",
"allow-get-users",
"allow-get-user",
"allow-remove-user",

View file

@ -4,10 +4,12 @@ use interpulse::api::minecraft::Version;
use onelauncher::constants::{NATIVE_ARCH, TARGET_OS, VERSION};
use onelauncher::data::{Loader, ManagedPackage, MinecraftCredentials, PackageData, Settings};
use onelauncher::package::content;
use onelauncher::store::{Cluster, ClusterPath, MinecraftLogin};
use onelauncher::store::{Cluster, ClusterPath};
use onelauncher::{cluster, minecraft, processor, settings};
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::AppHandle;
use tauri_plugin_shell::ShellExt;
use uuid::Uuid;
#[macro_export]
@ -21,8 +23,7 @@ macro_rules! collect_commands {
)
.commands(tauri_specta::collect_commands![
// User
begin_msa,
finish_msa,
msa_auth,
get_users,
get_user,
remove_user,
@ -110,16 +111,6 @@ pub async fn run_cluster(uuid: Uuid) -> Result<(Uuid, u32), String> {
.id()
.unwrap_or(0);
// tokio::task::spawn(async move {
// let mut proc = c_lock.write().await;
// if let Err(err) = processor::wait_for(&mut proc).await {
// tracing::error!("Error waiting for process: {:?}", err);
// };
// });
// let mut proc = c_lock.write().await;
// processor::wait_for(&mut proc).await?;
Ok((p_uuid, p_pid))
}
@ -160,12 +151,6 @@ pub async fn get_cluster(uuid: Uuid) -> Result<Cluster, String> {
#[specta::specta]
#[tauri::command]
pub async fn get_clusters() -> Result<Vec<Cluster>, String> {
// let mut map = HashMap::<ClusterPath, Cluster>::new();
// let cluster = placeholder_cluster();
// map.insert(cluster.cluster_path(), cluster);
// Ok(map)
Ok(cluster::list(None).await?)
}
@ -230,17 +215,13 @@ pub async fn get_user(uuid: Uuid) -> Result<MinecraftCredentials, String> {
#[specta::specta]
#[tauri::command]
pub async fn begin_msa() -> Result<MinecraftLogin, String> {
Ok(minecraft::begin().await?)
}
pub async fn msa_auth(handle: AppHandle) -> Result<MinecraftCredentials, String> {
let login = minecraft::begin().await?;
handle.shell().open(login.redirect_uri.clone(), None).map_err(|err| err.to_string())?;
#[specta::specta]
#[tauri::command]
pub async fn finish_msa(
code: String,
login: MinecraftLogin,
) -> Result<MinecraftCredentials, String> {
Ok(minecraft::finish(code.as_str(), login).await?)
Err("not implemented".to_string())
// Ok(minecraft::finish(code.as_str(), login).await?)
}
#[specta::specta]

View file

@ -5,7 +5,7 @@ function ScrollableContainer(props: ParentProps) {
return (
<div class="flex flex-col flex-1 overflow-hidden w-[calc(100%+14px)]">
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar relative flex-1">
<div class="flex flex-col absolute w-[calc(100%-14px)] gap-1">
<div class="flex flex-col absolute w-[calc(100%-14px)] h-full gap-1">
{props.children}
</div>
</OverlayScrollbarsComponent>

View file

@ -16,6 +16,10 @@
@apply bg-danger hover:bg-danger-hover active:bg-danger-pressed disabled:bg-danger-disabled;
}
&__iconDanger {
@apply bg-op-10;
}
&__ghost {
@apply hover:bg-gray-05 [&[aria-checked="true"]]:bg-gray-10 active:bg-gray-10;
}
@ -54,4 +58,4 @@
.icon > * {
@apply w-4;
}
}

View file

@ -1,84 +1,158 @@
import { ChevronRightIcon, InfoCircleIcon, UserPlus02Icon } from '@untitled-theme/icons-solid';
import { For, Show, createSignal, mergeProps } from 'solid-js';
import { InfoCircleIcon, LinkExternal01Icon, Trash01Icon, UserPlus02Icon } from '@untitled-theme/icons-solid';
import { type Accessor, For, Match, type Setter, Show, Switch, createEffect, createSignal, mergeProps } from 'solid-js';
import { bridge } from '~imports';
import Button from '~ui/components/base/Button';
import Tooltip from '~ui/components/base/Tooltip';
import PlayerHead from '~ui/components/game/PlayerHead';
import Modal from '~ui/components/overlay/Modal';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
const Accounts: {
username: string;
uuid: string;
}[] = [
{
username: 'Lynith',
uuid: 'f247be7c-5b82-41c6-9148-793ded77e71f',
},
{
username: 'Wyvest',
uuid: 'a5331404-0e77-440e-8bef-24c071dac1ae',
},
{
username: 'nextday',
uuid: '000646f6-b779-419c-b5d6-2c5ba52419bf',
},
{
username: 'xXfake_playerXx',
uuid: '000646f6-b779-419c-b5d6-aaa',
},
];
import useCommand from '~ui/hooks/useCommand';
function SettingsAccounts() {
const [current, setCurrent] = createSignal<string>();
const [accounts, { refetch }] = useCommand(bridge.commands.getUsers);
const [modalVisible, setModalVisible] = createSignal(false);
createEffect(() => {
const users = accounts();
if (users === undefined || users.length < 1 || current() !== undefined)
return;
setCurrent(users[0]!.id);
});
function onDelete(uuid: string) {
bridge.commands.removeUser(uuid)
.finally(() => {
refetch();
});
}
function showModal() {
setModalVisible(true);
}
return (
<Sidebar.Page>
<h1>Accounts</h1>
<ScrollableContainer>
<For each={Accounts}>
{account => (
<AccountRow
username={account.username}
uuid={account.uuid}
current={current() === account.uuid}
onClick={setCurrent}
/>
)}
</For>
<Switch>
<Match when={accounts()?.length === 0}>
<div class="flex flex-col h-full gap-y-4 max-h-64 justify-center items-center">
<span class="text-lg font-bold uppercase text-fg-secondary">No accounts added.</span>
<span class="text-xl font-bold">Add one with the Add Account button.</span>
</div>
</Match>
<Match when={accounts()?.length !== 0}>
<For each={accounts()}>
{account => (
<AccountRow
username={account.username}
uuid={account.id}
current={current() === account.id}
onClick={setCurrent}
onDelete={onDelete}
/>
)}
</For>
</Match>
</Switch>
</ScrollableContainer>
<div class="flex flex-row justify-end items-end mt-2">
<Button
buttonStyle="primary"
iconLeft={<UserPlus02Icon />}
children="Add Account"
onClick={() => {
// @ts-expect-error -- aa
window.doTheThing = bridge.comments.finishMsa;
bridge.commands.beginMsa().then((data) => {
if (data.status === 'ok') {
// @ts-expect-error -- aa
window.tempData = data.data;
// console.log('set');
}
else {
// console.log(data.error);
}
});
}}
onClick={showModal}
/>
</div>
<AddAccountModal
setVisible={setModalVisible}
visible={modalVisible}
refetch={refetch}
/>
</Sidebar.Page>
);
}
enum ModalStage {
Tasks,
WaitingForCode,
LoggingIn,
}
interface AddAccountModalProps {
visible: Accessor<boolean>;
setVisible: Setter<boolean>;
refetch: () => any;
}
function AddAccountModal(props: AddAccountModalProps) {
const [stage, setStage] = createSignal(ModalStage.Tasks);
function start() {
setStage(ModalStage.WaitingForCode);
// tryResult(bridge.commands.beginMsa).then((res) => {
// console.log(res);
// });
}
function finish() {
props.setVisible(false);
props.refetch();
}
return (
<Modal.Simple
title="Add Account"
visible={props.visible}
setVisible={props.setVisible}
buttons={[
<Button
buttonStyle="secondary"
children="Cancel"
onClick={() => props.setVisible(false)}
/>,
<Button
buttonStyle="primary"
children="Add"
iconLeft={<LinkExternal01Icon />}
onClick={start}
disabled={stage() !== 0}
/>,
]}
>
<div class="flex flex-col gap-y-3 max-w-120 line-height-normal">
<Switch>
<Match when={stage() !== ModalStage.LoggingIn}>
<p>
Pressing the "Add" button will open your browser with a Microsoft login page.
On this page, you login to your chosen Microsoft account and end up being asked whether you want to add the OneLauncher application.
</p>
</Match>
<Match when={stage() === ModalStage.LoggingIn}>
<p>Proceeding with Microsoft auth steps...</p>
</Match>
</Switch>
</div>
</Modal.Simple>
);
}
interface AccountRowProps {
username: string;
uuid: string;
current?: boolean;
onClick: (uuid: string) => any;
onDelete: (uuid: string) => any;
};
function AccountRow(props: AccountRowProps) {
@ -114,11 +188,11 @@ function AccountRow(props: AccountRowProps) {
<div class="">
<Button
buttonStyle="icon"
children={<ChevronRightIcon />}
buttonStyle="iconDanger"
children={<Trash01Icon />}
onClick={(e) => {
e.stopPropagation();
// TODO: Display fullscreen profile page
props.onDelete(props.uuid);
}}
/>
</div>

View file

@ -13,7 +13,7 @@ function SettingsAppearance() {
<Sidebar.Page>
<h1>Appearance</h1>
<ScrollableContainer>
<div class="flex flex-row items-center flex-1">
<div class="flex flex-row items-center">
<p>theme placeholder</p>
</div>

View file

@ -23,6 +23,8 @@ pub const DUMMY_REPLACE_NEWLINE: &str = "\n";
pub const DISCORD_RPC_CLIENT_ID: &str = "1234567890000000"; // todo
/// Our Microsoft client ID.
pub const MICROSOFT_CLIENT_ID: &str = "9eac3a4e-8cdd-43ef-863e-49cd601b1f03";
/// The port to run the localhost server on.
pub const MSA_SERVER_PORT: u16 = 13523;
/// Mojang/Microsoft client ID.
pub const MINECRAFT_CLIENT_ID: &str = "00000000402b5328";
/// Mojang/Microsoft login redirect URI.

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OneLauncher</title>
<style>
* {
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Ubuntu, Cantarell, sans-serif;
}
body {
opacity: 0;
animation: fadeIn 1s forwards 0.3s;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 80vh;
font-size: 24px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>
</head>
<body>
<h1>%TITLE%</h1>
<p>%CONTENT%</p>
</body>
</html>

View file

@ -310,6 +310,89 @@ impl MinecraftState {
}
}
// pub struct LocalServer {
// listener: TcpListener,
// sender: tokio::sync::mpsc::Sender<bool>,
// receiver: tokio::sync::mpsc::Receiver<bool>,
// }
// impl LocalServer {
// pub async fn new() -> crate::Result<Self> {
// let listener = TcpListener::bind(format!("127.0.0.1:{}", constants::MSA_SERVER_PORT)).await?;
// let (sender, receiver) = tokio::sync::mpsc::channel::<bool>(1);
// Ok(Self {
// listener,
// sender,
// receiver,
// })
// }
// pub async fn wait_for_code(&self) -> crate::Result<String> {
// }
// async fn handle_request(&self, stream: &mut TcpStream) -> crate::Result<String> {
// use tokio::io::{AsyncReadExt, AsyncWriteExt};
// let code: Option<String>;
// let code_result = {
// let mut buf = vec![0; 1024];
// let size = stream.read(&mut buf).await?;
// if size == 0 {
// return Err(MinecraftAuthError::MSACodeFetchError.into());
// }
// let request = String::from_utf8_lossy(&buf[..size]);
// code = Some(
// request
// .split(' ')
// .find(|x| x.starts_with("code="))
// .ok_or(anyhow::anyhow!("No code found"))?
// .split('=')
// .nth(1)
// .ok_or(anyhow::anyhow!("No code found"))?
// .to_string()
// );
// Ok::<(), String>(())
// };
// let errored = code_result.is_err();
// let html = include_str!("./auth_server.html");
// let result = match code_result {
// Ok(()) => {
// html
// .replace("%TITLE%", "Successfully Authenticated")
// .replace("%CONTENT%", "You can now close this tab.")
// }
// Err(e) => {
// html
// .replace("%TITLE%", "Error")
// .replace("%CONTENT%", &format!("An error has occurred: {}", e.as_str()))
// }
// };
// let response = format!(
// "HTTP/1.1 {}\r\nContent-Length: {}\r\n\r\n{}",
// if errored { "400 Bad Request" } else { "200 OK" },
// result.len(),
// result
// );
// stream.write_all(response.as_bytes()).await?;
// if errored {
// return Err(MinecraftAuthError::MSACodeFetchError.into());
// }
// match code {
// Some(code) => Ok(code),
// None => Err(MinecraftAuthError::MSACodeFetchError.into()),
// }
// }
// }
/// A [`reqwest::Request`] with a [`DateTime<Utc>`] attached to it.
struct RequestWithDate<T> {
pub date: DateTime<Utc>,
@ -1035,4 +1118,6 @@ pub enum MinecraftAuthError {
HashError,
#[error("failed to read user xbox session ID")]
SessionIdError,
#[error("failed to read user MSA code")]
MSACodeFetchError,
}