Add account stuff

This commit is contained in:
Lncvrt committed 2026-05-08 13:51:52 -07:00
1 parent dfc9b32264
commit 9e14aeaa0c
5 files changed
+402 -4

No files matched your search

+4 -1
View File
@@ -43,7 +43,10 @@
"allow": [ "allow": [
{ "url": "https://games.lncvrt.xyz/api/launcher/versions" }, { "url": "https://games.lncvrt.xyz/api/launcher/versions" },
{ "url": "https://games.lncvrt.xyz/api/launcher/download" }, { "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" }
] ]
} }
] ]
+342
View File
@@ -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<string>('')
const [password, setPassword] = useState<string>('')
return (
<>
<p className='text-3xl ml-4 mt-4'>Account Login</p>
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
<form
className='flex flex-col gap-2'
onSubmit={async e => {
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'
})
}
}}
>
<input
id='username'
name='username'
placeholder='Username'
type='username'
value={username}
onChange={e => setUsername(e.target.value)}
required
className='input-field my-1'
/>
<input
id='password'
name='password'
placeholder='Password'
type='password'
value={password}
onChange={e => setPassword(e.target.value)}
required
className='input-field my-1'
/>
<button type='submit' className='button btntheme1'>
Login
</button>
<div className='flex justify-center flex-col gap-2 mt-6 text-center'>
<button
type='button'
onClick={async () =>
await openUrl(
'https://games.lncvrt.xyz/account/forgot-username'
)
}
className='button btntheme1'
>
Forgot username?
</button>
<button
type='button'
onClick={async () =>
await openUrl(
'https://games.lncvrt.xyz/account/forgot-password'
)
}
className='button btntheme1'
>
Forgot password?
</button>
<button
type='button'
onClick={async () =>
await openUrl('https://games.lncvrt.xyz/account/register')
}
className='button btntheme1'
>
Don&apos;t have an account?
</button>
</div>
</form>
</div>
</>
)
}
function AccountChangeUsername ({
setChangingUsername
}: {
setChangingUsername: Dispatch<SetStateAction<boolean>>
}) {
const { account, accountSession } = useGlobal()
const [newUsername, setNewUsername] = useState<string>('')
return (
<>
<p className='text-3xl ml-4 mt-4'>Change Account Username</p>
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
<form
className='flex flex-col gap-2'
onSubmit={async e => {
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'
})
}
}}
>
<input
id='new-username'
name='new-username'
placeholder='New username'
type='username'
value={newUsername}
onChange={e => setNewUsername(e.target.value)}
required
className='input-field my-1'
/>
<button type='submit' className='button btntheme1'>
Change Username
</button>
<button
type='button'
className='button btntheme1'
onClick={() => setChangingUsername(false)}
>
Back
</button>
</form>
</div>
</>
)
}
function AccountChangePassword ({
setChangingPassword
}: {
setChangingPassword: Dispatch<SetStateAction<boolean>>
}) {
const { account, accountSession } = useGlobal()
const [newPassword, setNewPassword] = useState<string>('')
const [retypeNewPassword, setRetypeNewPassword] = useState<string>('')
return (
<>
<p className='text-3xl ml-4 mt-4'>Change Account Password</p>
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
<form
className='flex flex-col gap-2'
onSubmit={async e => {
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'
})
}
}}
>
<input
id='new-password'
name='new-password'
placeholder='New password'
type='password'
autoComplete='new-password'
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
required
className='input-field my-1'
/>
<input
id='retype-new-password'
name='retype-new-password'
placeholder='Re-type password'
type='password'
autoComplete='new-password'
value={retypeNewPassword}
onChange={e => setRetypeNewPassword(e.target.value)}
required
className='input-field my-1'
/>
<button type='submit' className='button btntheme1'>
Change Password
</button>
<button
type='button'
className='button btntheme1'
onClick={() => setChangingPassword(false)}
>
Back
</button>
</form>
</div>
</>
)
}
function AccountLoggedIn () {
const { account, accountName } = useGlobal()
const [changingUsername, setChangingUsername] = useState<boolean>(false)
const [changingPassword, setChangingPassword] = useState<boolean>(false)
if (changingUsername)
return <AccountChangeUsername setChangingUsername={setChangingUsername} />
if (changingPassword)
return <AccountChangePassword setChangingPassword={setChangingPassword} />
return (
<>
<p className='text-3xl ml-4 mt-4'>Account</p>
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
<p>Logged in as: {accountName}</p>
<div className='flex justify-center flex-col gap-2 mt-4 text-center'>
<button
onClick={() => setChangingUsername(true)}
className='button btntheme1'
>
Change username
</button>
<button
onClick={() => setChangingPassword(true)}
className='button btntheme1'
>
Change password
</button>
<button
onClick={async () => await account?.clear()}
className='button btntheme1'
>
Logout
</button>
</div>
</div>
</>
)
}
export default function AccountsPage () {
const { accountSession, accountId, accountName } = useGlobal()
if (!accountSession || !accountId || !accountName) return <AccountLoggedOut />
else return <AccountLoggedIn />
}
+34 -1
View File
@@ -86,6 +86,7 @@ export default function RootLayout ({
const [settings, setSettings] = useState<Store | null>(null) const [settings, setSettings] = useState<Store | null>(null)
const [versions, setVersions] = useState<Store | null>(null) const [versions, setVersions] = useState<Store | null>(null)
const [account, setAccount] = useState<Store | null>(null)
const [notificationsAllowed, setNotificationsAllowed] = const [notificationsAllowed, setNotificationsAllowed] =
useState<boolean>(false) useState<boolean>(false)
@@ -103,6 +104,11 @@ export default function RootLayout ({
Record<string, Record<string, number>> Record<string, Record<string, number>>
>({}) >({})
const [accountSession, setAccountSession] = useState<string | null>(null)
const [accountId, setAccountId] = useState<string | null>(null)
const [accountName, setAccountName] = useState<string | null>(null)
const [accountAdmin, setAccountAdmin] = useState<boolean>(false)
const downloadVersions = useCallback( const downloadVersions = useCallback(
async ( async (
list: Array<{ list: Array<{
@@ -658,6 +664,16 @@ export default function RootLayout ({
mods: {} 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<string>('version')) ?? client const cfgVer = (await settingsLocal.get<string>('version')) ?? client
const versVer = (await versionsLocal.get<string>('version')) ?? client const versVer = (await versionsLocal.get<string>('version')) ?? client
if (semver.gt(cfgVer, client) || semver.gt(versVer, client)) { if (semver.gt(cfgVer, client) || semver.gt(versVer, client)) {
@@ -667,6 +683,7 @@ export default function RootLayout ({
await settingsLocal.set('version', client) await settingsLocal.set('version', client)
await versionsLocal.set('version', client) await versionsLocal.set('version', client)
await accountLocal.set('version', client)
if (await exists('config.json', legacyOptions)) { if (await exists('config.json', legacyOptions)) {
const config = await readTextFile('config.json', legacyOptions) const config = await readTextFile('config.json', legacyOptions)
@@ -720,6 +737,7 @@ export default function RootLayout ({
} }
setSettings(settingsLocal) setSettings(settingsLocal)
setVersions(versionsLocal) setVersions(versionsLocal)
setAccount(accountLocal)
setLoading(false) setLoading(false)
if (!(await isPermissionGranted())) { if (!(await isPermissionGranted())) {
@@ -790,6 +808,11 @@ export default function RootLayout ({
unlisteners.push(versions!.onKeyChange<T>(key, fn)) unlisteners.push(versions!.onKeyChange<T>(key, fn))
} }
function watchAccount<T> (key: string, fn: (v: T | undefined) => void) {
account!.get<T>(key).then(fn)
unlisteners.push(account!.onKeyChange<T>(key, fn))
}
watchSettings<string>('theme', v => setTheme(v ?? 'dark')) watchSettings<string>('theme', v => setTheme(v ?? 'dark'))
watchSettings<boolean>('notificationsAllowed', v => watchSettings<boolean>('notificationsAllowed', v =>
setNotificationsAllowed(v ?? true) setNotificationsAllowed(v ?? true)
@@ -811,10 +834,15 @@ export default function RootLayout ({
setModsList(v ?? {}) setModsList(v ?? {})
) )
watchAccount<string | null>('session', v => setAccountSession(v ?? null))
watchAccount<string | null>('id', v => setAccountId(v ?? null))
watchAccount<string | null>('name', v => setAccountName(v ?? null))
watchAccount<boolean>('admin', v => setAccountAdmin(v ?? false))
return () => { return () => {
unlisteners.forEach(u => u.then(fn => fn())) unlisteners.forEach(u => u.then(fn => fn()))
} }
}, [settings, versions]) }, [settings, versions, account])
return ( return (
<> <>
@@ -867,6 +895,7 @@ export default function RootLayout ({
getSpecialVersionsList, getSpecialVersionsList,
settings, settings,
versions, versions,
account,
notificationsAllowed, notificationsAllowed,
sidebarAlwaysShowGames, sidebarAlwaysShowGames,
linuxUseWine, linuxUseWine,
@@ -876,6 +905,10 @@ export default function RootLayout ({
developerMode, developerMode,
versionsList, versionsList,
modsList, modsList,
accountSession,
accountId,
accountName,
accountAdmin,
movingData, movingData,
setMovingData, setMovingData,
managingGame, managingGame,
+17 -2
View File
@@ -9,7 +9,9 @@ import {
faDownload, faDownload,
faGamepad, faGamepad,
faHexagonNodes, faHexagonNodes,
faLayerGroup faLayerGroup,
faUser,
faUserShield
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { faDiscord } from '@fortawesome/free-brands-svg-icons' import { faDiscord } from '@fortawesome/free-brands-svg-icons'
import { platform } from '@tauri-apps/plugin-os' import { platform } from '@tauri-apps/plugin-os'
@@ -40,7 +42,9 @@ export default function Sidebar () {
linuxUseWine, linuxUseWine,
setManagingGame, setManagingGame,
settings, settings,
developerMode developerMode,
accountName,
accountAdmin
} = useGlobal() } = useGlobal()
const pathname = usePathname() const pathname = usePathname()
@@ -222,6 +226,17 @@ export default function Sidebar () {
> >
<FontAwesomeIcon icon={faCog} className='mr-1' /> Settings <FontAwesomeIcon icon={faCog} className='mr-1' /> Settings
</Link> </Link>
<Link
draggable={false}
href='/main/account'
className={`link ${pathname === '/main/account' ? 'active' : ''}`}
>
<FontAwesomeIcon
icon={accountAdmin ? faUserShield : faUser}
className='mr-1'
/>{' '}
{accountName ?? 'Account'}
</Link>
<button <button
onClick={() => openUrl('https://games.lncvrt.xyz/discord')} onClick={() => openUrl('https://games.lncvrt.xyz/discord')}
className='link mr-auto' className='link mr-auto'
+5
View File
@@ -52,6 +52,7 @@ type GlobalCtxType = {
getSpecialVersionsList: (game?: number) => GameVersion[] getSpecialVersionsList: (game?: number) => GameVersion[]
settings: Store | null settings: Store | null
versions: Store | null versions: Store | null
account: Store | null
notificationsAllowed: boolean notificationsAllowed: boolean
sidebarAlwaysShowGames: boolean sidebarAlwaysShowGames: boolean
linuxUseWine: boolean linuxUseWine: boolean
@@ -61,6 +62,10 @@ type GlobalCtxType = {
developerMode: boolean developerMode: boolean
versionsList: Record<string, number> versionsList: Record<string, number>
modsList: Record<string, Record<string, number>> modsList: Record<string, Record<string, number>>
accountSession: string | null
accountId: string | null
accountName: string | null
accountAdmin: boolean
movingData: boolean movingData: boolean
setMovingData: Dispatch<SetStateAction<boolean>> setMovingData: Dispatch<SetStateAction<boolean>>
managingGame: number | null managingGame: number | null