diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index bbc0056..a03efc8 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -43,7 +43,10 @@ "allow": [ { "url": "https://games.lncvrt.xyz/api/launcher/versions" }, { "url": "https://games.lncvrt.xyz/api/launcher/download" }, - { "url": "https://games.lncvrt.xyz/api/launcher/mods" } + { "url": "https://games.lncvrt.xyz/api/launcher/mods" }, + { "url": "https://games.lncvrt.xyz/api/account/login" }, + { "url": "https://games.lncvrt.xyz/api/account/change-username" }, + { "url": "https://games.lncvrt.xyz/api/account/change-password" } ] } ] diff --git a/src/app/main/account/page.tsx b/src/app/main/account/page.tsx new file mode 100644 index 0000000..e7ac8e2 --- /dev/null +++ b/src/app/main/account/page.tsx @@ -0,0 +1,342 @@ +'use client' + +import { verifySignature } from '@/lib/util' +import { useGlobal } from '@/providers/GlobalProvider' +import { message } from '@tauri-apps/plugin-dialog' +import { fetch } from '@tauri-apps/plugin-http' +import { openUrl } from '@tauri-apps/plugin-opener' +import { Dispatch, SetStateAction, useState } from 'react' + +function AccountLoggedOut () { + const { account } = useGlobal() + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + + return ( + <> +

Account Login

+
+
{ + e.preventDefault() + + const response = await fetch( + 'https://games.lncvrt.xyz/api/account/login', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + username, + password + }) + } + ) + const signature = response.headers.get('x-signature') ?? '' + const data = await response.json() + if (!(await verifySignature(JSON.stringify(data), signature))) { + await message( + "Failed to login, there was an issue validating the data wasn't tampered with.", + { title: 'Request tampered', kind: 'error' } + ) + return + } + if (data.success) { + if (data.data.session) + await account?.set('session', data.data.session) + if (data.data.username) + await account?.set('name', data.data.username) + if (data.data.id) await account?.set('id', data.data.id) + if (data.data.admin) await account?.set('admin', data.data.admin) + } else { + await message(data.message || 'n/a', { + title: 'Failed to login', + kind: 'error' + }) + } + }} + > + setUsername(e.target.value)} + required + className='input-field my-1' + /> + setPassword(e.target.value)} + required + className='input-field my-1' + /> + +
+ + + +
+
+
+ + ) +} + +function AccountChangeUsername ({ + setChangingUsername +}: { + setChangingUsername: Dispatch> +}) { + const { account, accountSession } = useGlobal() + + const [newUsername, setNewUsername] = useState('') + + return ( + <> +

Change Account Username

+
+
{ + e.preventDefault() + + const response = await fetch( + 'https://games.lncvrt.xyz/api/account/change-username', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: accountSession ?? '' + }, + body: JSON.stringify({ + newUsername + }) + } + ) + const signature = response.headers.get('x-signature') ?? '' + const data = await response.json() + if (!(await verifySignature(JSON.stringify(data), signature))) { + await message( + "Failed to change username, there was an issue validating the data wasn't tampered with.", + { title: 'Request tampered', kind: 'error' } + ) + return + } + if (data.success) { + if (data.data) await account?.set('session', data.data) + await account?.set('name', newUsername) + setChangingUsername(false) + } else { + await message(data.message || 'n/a', { + title: 'Failed to change username', + kind: 'error' + }) + } + }} + > + setNewUsername(e.target.value)} + required + className='input-field my-1' + /> + + +
+
+ + ) +} + +function AccountChangePassword ({ + setChangingPassword +}: { + setChangingPassword: Dispatch> +}) { + const { account, accountSession } = useGlobal() + + const [newPassword, setNewPassword] = useState('') + const [retypeNewPassword, setRetypeNewPassword] = useState('') + + return ( + <> +

Change Account Password

+
+
{ + e.preventDefault() + + if (newPassword !== retypeNewPassword) { + alert('Passwords must match') + return + } + + const response = await fetch( + 'https://games.lncvrt.xyz/api/account/change-password', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: accountSession ?? '' + }, + body: JSON.stringify({ + newPassword + }) + } + ) + const signature = response.headers.get('x-signature') ?? '' + const data = await response.json() + if (!(await verifySignature(JSON.stringify(data), signature))) { + await message( + "Failed to change password, there was an issue validating the data wasn't tampered with.", + { title: 'Request tampered', kind: 'error' } + ) + return + } + if (data.success) { + if (data.data) await account?.set('session', data.data) + setChangingPassword(false) + } else { + await message(data.message || 'n/a', { + title: 'Failed to change password', + kind: 'error' + }) + } + }} + > + setNewPassword(e.target.value)} + required + className='input-field my-1' + /> + setRetypeNewPassword(e.target.value)} + required + className='input-field my-1' + /> + + +
+
+ + ) +} + +function AccountLoggedIn () { + const { account, accountName } = useGlobal() + const [changingUsername, setChangingUsername] = useState(false) + const [changingPassword, setChangingPassword] = useState(false) + + if (changingUsername) + return + + if (changingPassword) + return + + return ( + <> +

Account

+
+

Logged in as: {accountName}

+
+ + + +
+
+ + ) +} + +export default function AccountsPage () { + const { accountSession, accountId, accountName } = useGlobal() + + if (!accountSession || !accountId || !accountName) return + else return +} diff --git a/src/app/main/layout.tsx b/src/app/main/layout.tsx index 162abb4..de2fba9 100644 --- a/src/app/main/layout.tsx +++ b/src/app/main/layout.tsx @@ -86,6 +86,7 @@ export default function RootLayout ({ const [settings, setSettings] = useState(null) const [versions, setVersions] = useState(null) + const [account, setAccount] = useState(null) const [notificationsAllowed, setNotificationsAllowed] = useState(false) @@ -103,6 +104,11 @@ export default function RootLayout ({ Record> >({}) + const [accountSession, setAccountSession] = useState(null) + const [accountId, setAccountId] = useState(null) + const [accountName, setAccountName] = useState(null) + const [accountAdmin, setAccountAdmin] = useState(false) + const downloadVersions = useCallback( async ( list: Array<{ @@ -658,6 +664,16 @@ export default function RootLayout ({ mods: {} } }) + const accountLocal = await load('account.json', { + autoSave: true, + defaults: { + version: client, + session: null, + id: null, + name: null, + admin: false + } + }) const cfgVer = (await settingsLocal.get('version')) ?? client const versVer = (await versionsLocal.get('version')) ?? client if (semver.gt(cfgVer, client) || semver.gt(versVer, client)) { @@ -667,6 +683,7 @@ export default function RootLayout ({ await settingsLocal.set('version', client) await versionsLocal.set('version', client) + await accountLocal.set('version', client) if (await exists('config.json', legacyOptions)) { const config = await readTextFile('config.json', legacyOptions) @@ -720,6 +737,7 @@ export default function RootLayout ({ } setSettings(settingsLocal) setVersions(versionsLocal) + setAccount(accountLocal) setLoading(false) if (!(await isPermissionGranted())) { @@ -790,6 +808,11 @@ export default function RootLayout ({ unlisteners.push(versions!.onKeyChange(key, fn)) } + function watchAccount (key: string, fn: (v: T | undefined) => void) { + account!.get(key).then(fn) + unlisteners.push(account!.onKeyChange(key, fn)) + } + watchSettings('theme', v => setTheme(v ?? 'dark')) watchSettings('notificationsAllowed', v => setNotificationsAllowed(v ?? true) @@ -811,10 +834,15 @@ export default function RootLayout ({ setModsList(v ?? {}) ) + watchAccount('session', v => setAccountSession(v ?? null)) + watchAccount('id', v => setAccountId(v ?? null)) + watchAccount('name', v => setAccountName(v ?? null)) + watchAccount('admin', v => setAccountAdmin(v ?? false)) + return () => { unlisteners.forEach(u => u.then(fn => fn())) } - }, [settings, versions]) + }, [settings, versions, account]) return ( <> @@ -867,6 +895,7 @@ export default function RootLayout ({ getSpecialVersionsList, settings, versions, + account, notificationsAllowed, sidebarAlwaysShowGames, linuxUseWine, @@ -876,6 +905,10 @@ export default function RootLayout ({ developerMode, versionsList, modsList, + accountSession, + accountId, + accountName, + accountAdmin, movingData, setMovingData, managingGame, diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d61e7cf..51fea87 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -9,7 +9,9 @@ import { faDownload, faGamepad, faHexagonNodes, - faLayerGroup + faLayerGroup, + faUser, + faUserShield } from '@fortawesome/free-solid-svg-icons' import { faDiscord } from '@fortawesome/free-brands-svg-icons' import { platform } from '@tauri-apps/plugin-os' @@ -40,7 +42,9 @@ export default function Sidebar () { linuxUseWine, setManagingGame, settings, - developerMode + developerMode, + accountName, + accountAdmin } = useGlobal() const pathname = usePathname() @@ -222,6 +226,17 @@ export default function Sidebar () { > Settings + + {' '} + {accountName ?? 'Account'} +