feat: Account page fixes + ready to integrate with auth
This commit is contained in:
parent
05e6db5b1d
commit
4539cb5388
10 changed files with 266 additions and 89 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
33
packages/core/src/store/auth_server.html
Normal file
33
packages/core/src/store/auth_server.html
Normal 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>
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue