Implement authentication features with login, registration, and user management; update service configurations for development and production environments.

This commit is contained in:
2026-01-23 01:50:31 +00:00
parent de14c646fa
commit 8e92c28272
9 changed files with 637 additions and 178 deletions

View File

@@ -17,6 +17,8 @@ Environment="HOST=0.0.0.0"
Environment="WDS_SOCKET_PROTOCOL=wss" Environment="WDS_SOCKET_PROTOCOL=wss"
Environment="WDS_SOCKET_HOST=dev.leagues.tools" Environment="WDS_SOCKET_HOST=dev.leagues.tools"
Environment="WDS_SOCKET_PORT=443" Environment="WDS_SOCKET_PORT=443"
Environment="REACT_APP_RELDO_URL=https://api.leagues.tools"
# Start the dev server with hot reload # Start the dev server with hot reload
ExecStart=/usr/bin/npm run dev ExecStart=/usr/bin/npm run dev

View File

@@ -0,0 +1,74 @@
const BASE_URL = process.env.REACT_APP_RELDO_URL || 'http://localhost:8080';
const DEFAULT_HEADERS = {
'Content-type': 'application/json',
};
function handleResponse(response) {
if (!response.ok) {
return response.json().then(data => ({
success: false,
error: data.message || data.error || 'An error occurred',
}));
}
return response.json().then(data => ({
success: true,
value: data,
}));
}
function handleError(error) {
console.warn(error);
return { success: false, error: error.message || 'Network error' };
}
export function login(username, password) {
return fetch(`${BASE_URL}/api/login`, {
method: 'POST',
headers: DEFAULT_HEADERS,
credentials: 'include',
body: JSON.stringify({ username, password }),
})
.then(handleResponse)
.catch(handleError);
}
export function register(username, email, password) {
return fetch(`${BASE_URL}/api/register`, {
method: 'POST',
headers: DEFAULT_HEADERS,
credentials: 'include',
body: JSON.stringify({ username, email, password }),
})
.then(handleResponse)
.catch(handleError);
}
export function logout() {
return fetch(`${BASE_URL}/api/logout`, {
method: 'POST',
headers: DEFAULT_HEADERS,
credentials: 'include',
})
.then(handleResponse)
.catch(handleError);
}
export function getAuthStatus() {
return fetch(`${BASE_URL}/api/auth/status`, {
method: 'GET',
headers: DEFAULT_HEADERS,
credentials: 'include',
})
.then(handleResponse)
.catch(handleError);
}
export function getCurrentUser() {
return fetch(`${BASE_URL}/api/me`, {
method: 'GET',
headers: DEFAULT_HEADERS,
credentials: 'include',
})
.then(handleResponse)
.catch(handleError);
}

View File

@@ -0,0 +1,255 @@
import React, { useState } from 'react';
import Modal from './Modal';
import Spinner from './common/Spinner';
import { login, register } from '../client/auth-client';
const VIEW = {
LOGIN: 'login',
REGISTER: 'register',
};
export default function AuthModal({ isOpen, setIsOpen, onAuthSuccess }) {
const [view, setView] = useState(VIEW.LOGIN);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [loginUsername, setLoginUsername] = useState('');
const [loginPassword, setLoginPassword] = useState('');
const [registerUsername, setRegisterUsername] = useState('');
const [registerEmail, setRegisterEmail] = useState('');
const [registerPassword, setRegisterPassword] = useState('');
const [registerConfirmPassword, setRegisterConfirmPassword] = useState('');
const resetForm = () => {
setLoginUsername('');
setLoginPassword('');
setRegisterUsername('');
setRegisterEmail('');
setRegisterPassword('');
setRegisterConfirmPassword('');
setError('');
};
const handleClose = () => {
resetForm();
setView(VIEW.LOGIN);
};
const switchView = newView => {
resetForm();
setView(newView);
};
const handleLogin = async e => {
e.preventDefault();
setError('');
if (!loginUsername || !loginPassword) {
setError('Please fill in all fields');
return;
}
setIsLoading(true);
const result = await login(loginUsername, loginPassword);
setIsLoading(false);
if (result.success) {
resetForm();
setIsOpen(false);
if (onAuthSuccess) {
onAuthSuccess(result.value);
}
} else {
setError(result.error || 'Login failed');
}
};
const handleRegister = async e => {
e.preventDefault();
setError('');
if (!registerUsername || !registerEmail || !registerPassword || !registerConfirmPassword) {
setError('Please fill in all fields');
return;
}
if (registerPassword !== registerConfirmPassword) {
setError('Passwords do not match');
return;
}
if (registerPassword.length < 8) {
setError('Password must be at least 8 characters');
return;
}
setIsLoading(true);
const result = await register(registerUsername, registerEmail, registerPassword);
setIsLoading(false);
if (result.success) {
resetForm();
setIsOpen(false);
if (onAuthSuccess) {
onAuthSuccess(result.value);
}
} else {
setError(result.error || 'Registration failed');
}
};
return (
<Modal
isOpen={isOpen}
setIsOpen={setIsOpen}
onClose={handleClose}
className='w-96 shadow shadow-primary rounded-md bg-primary-alt'
>
<Modal.Header className='text-center small-caps tracking-wide text-xl text-accent font-semibold'>
{view === VIEW.LOGIN ? 'Login' : 'Create Account'}
</Modal.Header>
<Modal.Body className='text-primary text-sm'>
{view === VIEW.LOGIN ? (
<form onSubmit={handleLogin} className='m-4 flex flex-col gap-3'>
<label htmlFor='login-username' className='flex flex-col gap-1'>
<span className='text-secondary text-xs'>Username</span>
<input
id='login-username'
name='username'
type='text'
className='input-primary text-sm form-input'
placeholder='Enter username'
value={loginUsername}
onChange={e => setLoginUsername(e.target.value)}
disabled={isLoading}
autoComplete='username'
/>
</label>
<label htmlFor='login-password' className='flex flex-col gap-1'>
<span className='text-secondary text-xs'>Password</span>
<input
id='login-password'
name='password'
type='password'
className='input-primary text-sm form-input'
placeholder='Enter password'
value={loginPassword}
onChange={e => setLoginPassword(e.target.value)}
disabled={isLoading}
autoComplete='current-password'
/>
</label>
{error && <div className='text-error text-xs text-center'>{error}</div>}
<button type='submit' className='button-filled py-2 mt-2' disabled={isLoading}>
{isLoading ? (
<span className='flex items-center justify-center gap-2'>
<Spinner size={Spinner.SIZE.sm} /> Logging in...
</span>
) : (
'Login'
)}
</button>
</form>
) : (
<form onSubmit={handleRegister} className='m-4 flex flex-col gap-3'>
<label htmlFor='register-username' className='flex flex-col gap-1'>
<span className='text-secondary text-xs'>Username</span>
<input
id='register-username'
name='username'
type='text'
className='input-primary text-sm form-input'
placeholder='Choose a username'
value={registerUsername}
onChange={e => setRegisterUsername(e.target.value)}
disabled={isLoading}
autoComplete='username'
/>
</label>
<label htmlFor='register-email' className='flex flex-col gap-1'>
<span className='text-secondary text-xs'>Email</span>
<input
id='register-email'
name='email'
type='email'
className='input-primary text-sm form-input'
placeholder='Enter your email'
value={registerEmail}
onChange={e => setRegisterEmail(e.target.value)}
disabled={isLoading}
autoComplete='email'
/>
</label>
<label htmlFor='register-password' className='flex flex-col gap-1'>
<span className='text-secondary text-xs'>Password</span>
<input
id='register-password'
name='new-password'
type='password'
className='input-primary text-sm form-input'
placeholder='Create a password'
value={registerPassword}
onChange={e => setRegisterPassword(e.target.value)}
disabled={isLoading}
autoComplete='new-password'
/>
</label>
<label htmlFor='register-confirm-password' className='flex flex-col gap-1'>
<span className='text-secondary text-xs'>Confirm Password</span>
<input
id='register-confirm-password'
name='confirm-password'
type='password'
className='input-primary text-sm form-input'
placeholder='Confirm your password'
value={registerConfirmPassword}
onChange={e => setRegisterConfirmPassword(e.target.value)}
disabled={isLoading}
autoComplete='new-password'
/>
</label>
{error && <div className='text-error text-xs text-center'>{error}</div>}
<button type='submit' className='button-filled py-2 mt-2' disabled={isLoading}>
{isLoading ? (
<span className='flex items-center justify-center gap-2'>
<Spinner size={Spinner.SIZE.sm} /> Creating account...
</span>
) : (
'Create Account'
)}
</button>
</form>
)}
</Modal.Body>
<Modal.Footer className='text-center text-sm'>
{view === VIEW.LOGIN ? (
<p className='text-secondary py-2'>
Don&apos;t have an account?{' '}
<button type='button' className='text-accent hover:underline' onClick={() => switchView(VIEW.REGISTER)}>
Register
</button>
</p>
) : (
<p className='text-secondary py-2'>
Already have an account?{' '}
<button type='button' className='text-accent hover:underline' onClick={() => switchView(VIEW.LOGIN)}>
Login
</button>
</p>
)}
</Modal.Footer>
</Modal>
);
}

View File

@@ -7,9 +7,9 @@ import FeedbackModal from './FeedbackModal';
import ManageDataModal from './ManageDataModal'; import ManageDataModal from './ManageDataModal';
import images from '../assets/images'; import images from '../assets/images';
import useQueryString from '../hooks/useQueryString'; import useQueryString from '../hooks/useQueryString';
// import ManageData from './nav/ManageData';
import Feedback from './nav/Feedback'; import Feedback from './nav/Feedback';
// import Character from './nav/Character'; import AuthButton from './nav/AuthButton';
import Character from './nav/Character';
import ManageCharactersModal from './ManageCharactersModal'; import ManageCharactersModal from './ManageCharactersModal';
export default function PageWrapper({ children }) { export default function PageWrapper({ children }) {
@@ -18,9 +18,6 @@ export default function PageWrapper({ children }) {
const [isFeedbackModalOpen, setFeedbackModalOpen] = useState(false); const [isFeedbackModalOpen, setFeedbackModalOpen] = useState(false);
const [isCharacterModalOpen, setCharacterModalOpen] = useState(false); const [isCharacterModalOpen, setCharacterModalOpen] = useState(false);
const [manageDataModalType, setManageDataModalType] = useQueryString('open'); const [manageDataModalType, setManageDataModalType] = useQueryString('open');
const user = useSelector(state => state.auth?.user); // adjust to your store
const selectedCharacter = useSelector(state => state.character?.selected); // adjust
const navItems = [ const navItems = [
new NavItem('Stats', 'primary', 0, 0).withRouterLink('/stats').withIconFont('query_stats'), new NavItem('Stats', 'primary', 0, 0).withRouterLink('/stats').withIconFont('query_stats'),
@@ -45,49 +42,25 @@ export default function PageWrapper({ children }) {
/> />
) )
), ),
new NavItem('Profile', 'footer', 10, 0).withRouterLink('/profile').withIconFont('account_circle'), new NavItem('Account', 'footer', 10, 0).withCustomRenderFn(
new NavItem('User', 'footer', 10, 1).withCustomRenderFn(
(isCollapsed, onNavigate) => ( (isCollapsed, onNavigate) => (
<div <AuthButton.SideBarItem
key='footer-user' key='auth'
className='sidebar-nav-link' isCollapsed={isCollapsed}
onClick={() => { onNavigate={onNavigate}
// optional: if you want clicking the row to go to profile />
// navigate('/profile') or use a NavLink style row
onNavigate?.();
}}
title={isCollapsed ? (user?.name ?? user?.email ?? 'Profile') : undefined}
>
<span className='sidebar-nav-link-icon icon-base'>person</span>
{!isCollapsed && (
<span className='sidebar-nav-link-label'>
{user?.name ?? user?.email ?? 'Unknown user'}
</span>
)}
</div>
) )
), ),
new NavItem('Selected Character', 'footer', 10, 2).withCustomRenderFn( new NavItem('Character', 'footer', 10, 1).withCustomRenderFn(
(isCollapsed, onNavigate) => ( (isCollapsed, onNavigate) => (
<button <Character.SideBarItem
key='footer-selected-character' key='character'
type='button' setCharacterModalOpen={setCharacterModalOpen}
className='sidebar-nav-link w-full text-left' isCollapsed={isCollapsed}
onClick={() => { onNavigate={onNavigate}
setCharacterModalOpen(true); />
onNavigate?.();
}}
title={isCollapsed ? (selectedCharacter?.name ?? 'Select character') : undefined}
>
<span className='sidebar-nav-link-icon icon-base'>badge</span>
{!isCollapsed && (
<span className='sidebar-nav-link-label'>
{selectedCharacter?.name ?? 'Select character'}
</span>
)}
</button>
) )
), ),
// new NavItem('Tip Jar', 'overflow', 4, 3) // new NavItem('Tip Jar', 'overflow', 4, 3)
// .withHref('https://ko-fi.com/osleaguetools', '_blank') // .withHref('https://ko-fi.com/osleaguetools', '_blank')
// .withIconFont('savings'), // .withIconFont('savings'),

View File

@@ -1,9 +1,26 @@
import React from 'react'; import React, { useRef, useState } from 'react';
import Spinner from '../common/Spinner'; import Spinner from '../common/Spinner';
import Dropdown from '../common/Dropdown';
import useAccount from '../../hooks/useAccount'; import useAccount from '../../hooks/useAccount';
import useClickListener from '../../hooks/useClickListener';
import AuthModal from '../AuthModal';
function NavBarItem() { function NavBarItem() {
const { isLoggedIn, isAuthenticating, login, logout } = useAccount({ redirectReturnToUrl: window.location.origin }); const [isExpanded, setExpanded] = useState(false);
const menuRef = useRef(null);
const {
isLoggedIn,
isAuthenticating,
username,
userEmail,
openAuthModal,
logout,
authModalOpen,
closeAuthModal,
handleAuthSuccess,
} = useAccount();
useClickListener(menuRef, () => setExpanded(false), true);
if (isAuthenticating) { if (isAuthenticating) {
return ( return (
@@ -14,41 +31,198 @@ function NavBarItem() {
); );
} }
const { label, icon, action } = getButtonValues(isLoggedIn, login, logout); if (!isLoggedIn) {
return ( return (
<> <>
<button <button
className='text-primary md:inline hidden navbar-link-alt bg-hover px-2 py-1' className='text-primary md:inline hidden navbar-link-alt bg-hover px-2 py-1'
onClick={() => action()} onClick={openAuthModal}
type='button' type='button'
> >
{label} <span className='icon-base mr-1 align-bottom'>login</span>
Login
</button> </button>
<button className='md:hidden inline navbar-icon-link' onClick={action} type='button'> <button className='md:hidden inline navbar-icon-link' onClick={openAuthModal} type='button'>
<span className='text-primary-alt icon-lg leading-tight align-middle'>{icon}</span> <span className='text-primary-alt icon-lg leading-tight align-middle'>login</span>
</button>
<AuthModal isOpen={authModalOpen} setIsOpen={closeAuthModal} onAuthSuccess={handleAuthSuccess} />
</>
);
}
return (
<div className='relative'>
<button
className='text-primary md:inline hidden navbar-link-alt bg-hover py-1 px-2'
type='button'
onClick={() => setExpanded(true)}
>
<span className='text-primary-alt icon-lg inline align-middle mr-1'>account_circle</span>
{username}
</button>
<button className='md:hidden inline navbar-icon-link' onClick={() => setExpanded(true)} type='button'>
<span className='text-primary-alt icon-lg leading-tight align-middle'>account_circle</span>
</button>
<div className='mt-1 absolute right-0 text-center'>
<Dropdown show={isExpanded} innerRef={menuRef}>
<Dropdown.Text isHeading>
<span className='text-accent'>{username}</span>
{userEmail && <p className='text-xs text-secondary font-normal'>{userEmail}</p>}
</Dropdown.Text>
<Dropdown.Separator />
<Dropdown.Button
className='text-left'
icon='logout'
onClick={() => {
logout();
setExpanded(false);
}}
>
Logout
</Dropdown.Button>
</Dropdown>
</div>
</div>
);
}
function CollapsedMenu() {
const {
isLoggedIn,
isAuthenticating,
username,
openAuthModal,
logout,
authModalOpen,
closeAuthModal,
handleAuthSuccess,
} = useAccount();
if (isAuthenticating) {
return (
<div className='text-primary bg-hover py-1 text-left'>
<Spinner size={Spinner.SIZE.sm} />
<p className='h-4 inline pl-1 font-sans-alt'>Loading...</p>
</div>
);
}
if (!isLoggedIn) {
return (
<>
<button className='text-primary bg-hover py-1 text-left' onClick={openAuthModal} type='button'>
<span className='text-primary-alt icon-lg inline align-middle mr-1'>login</span>
<p className='h-4 inline pl-1 font-sans-alt'>Login</p>
</button>
<AuthModal isOpen={authModalOpen} setIsOpen={closeAuthModal} onAuthSuccess={handleAuthSuccess} />
</>
);
}
return (
<>
<div className='text-primary py-1 text-left'>
<span className='text-primary-alt icon-lg inline align-middle mr-1'>account_circle</span>
<p className='h-4 inline pl-1 font-sans-alt text-accent'>{username}</p>
</div>
<button className='text-primary bg-hover py-1 text-left' onClick={logout} type='button'>
<span className='text-primary-alt icon-lg inline align-middle mr-1'>logout</span>
<p className='h-4 inline pl-1 font-sans-alt'>Logout</p>
</button> </button>
</> </>
); );
} }
function CollapsedMenu() { function SideBarItem({ isCollapsed, onNavigate }) {
const { isLoggedIn, login, logout } = useAccount({ redirectReturnToUrl: window.location.origin }); const [isExpanded, setExpanded] = useState(false);
const { label, icon, action } = getButtonValues(isLoggedIn, login, logout); const menuRef = useRef(null);
const {
isLoggedIn,
isAuthenticating,
username,
userEmail,
openAuthModal,
logout,
authModalOpen,
closeAuthModal,
handleAuthSuccess,
} = useAccount();
useClickListener(menuRef, () => setExpanded(false), true);
if (isAuthenticating) {
return (
<div className='sidebar-nav-link w-full text-left'>
<Spinner size={Spinner.SIZE.sm} />
{!isCollapsed && <span className='sidebar-nav-link-label'>Loading...</span>}
</div>
);
}
if (!isLoggedIn) {
return (
<>
<button
className='sidebar-nav-link w-full text-left'
type='button'
onClick={() => {
openAuthModal();
if (onNavigate) {
onNavigate();
}
}}
title={isCollapsed ? 'Login' : undefined}
>
<span className='sidebar-nav-link-icon icon-base'>login</span>
{!isCollapsed && <span className='sidebar-nav-link-label'>Login</span>}
</button>
<AuthModal isOpen={authModalOpen} setIsOpen={closeAuthModal} onAuthSuccess={handleAuthSuccess} />
</>
);
}
return ( return (
<button className='text-primary bg-hover py-1 text-left' onClick={() => action()} type='button'> <div className='relative' ref={menuRef}>
<span className='text-primary-alt icon-lg inline align-middle mr-1'>{icon}</span> <button
<p className='h-4 inline pl-1 font-sans-alt'>{label}</p> className='sidebar-nav-link w-full text-left'
type='button'
onClick={() => setExpanded(!isExpanded)}
title={isCollapsed ? username : undefined}
>
<span className='sidebar-nav-link-icon icon-base'>account_circle</span>
{!isCollapsed && (
<>
<span className='sidebar-nav-link-label'>{username}</span>
<span className='ml-auto icon-sm text-primary-alt'>{isExpanded ? 'expand_less' : 'expand_more'}</span>
</>
)}
</button> </button>
{isExpanded && !isCollapsed && (
<div className='bg-secondary-alt pl-4'>
{userEmail && (
<div className='sidebar-nav-link w-full text-left text-sm text-secondary'>
<span className='sidebar-nav-link-icon icon-base text-primary-alt'>mail</span>
<span className='sidebar-nav-link-label text-xs'>{userEmail}</span>
</div>
)}
<button
className='sidebar-nav-link w-full text-left text-sm'
onClick={() => {
logout();
setExpanded(false);
if (onNavigate) {
onNavigate();
}
}}
type='button'
>
<span className='sidebar-nav-link-icon icon-base text-primary-alt'>logout</span>
<span className='sidebar-nav-link-label'>Logout</span>
</button>
</div>
)}
</div>
); );
} }
function getButtonValues(isLoggedIn, login, logout) { export default { NavBarItem, CollapsedMenu, SideBarItem };
return {
label: isLoggedIn ? 'Logout' : 'Login',
icon: isLoggedIn ? 'logout' : 'login',
action: isLoggedIn ? logout : login,
};
}
export default { NavBarItem, CollapsedMenu };

View File

@@ -1,101 +1,68 @@
import { useAuth0 } from '@auth0/auth0-react'; import { useEffect, useState } from 'react';
import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux';
import { useDispatch, useSelector, batch } from 'react-redux';
import { resetState } from '../store/common'; import { resetState } from '../store/common';
import { load as loadSettingsState, loadState as loadSettingsLocalState } from '../store/settings/settings'; import { setLoggedIn, setLoggedOut } from '../store/user/account';
import { load as loadTasksState, loadState as loadTasksLocalState } from '../store/tasks/tasks'; import { getAuthStatus, getCurrentUser, logout as logoutApi } from '../client/auth-client';
import { load as loadUnlocksState, loadState as loadUnlocksLocalState } from '../store/unlocks/unlocks';
import {
fetchHiscores,
load as loadCharacterState,
loadState as loadCharacterLocalState,
} from '../store/user/character';
import { updateAccountCache } from '../store/user/account';
import { createUserIfNeeded, getUser } from '../client/user-data-client';
import { INITIAL_STATE as INITIAL_TASKS_STATE } from '../store/tasks/constants';
import { INITIAL_STATE as INITIAL_UNLOCKS_STATE } from '../store/unlocks/constants';
import { INITIAL_STATE as INITIAL_CHARACTER_STATE , INITIAL_STATE as INITIAL_SETTINGS_STATE } from '../store/user/constants';
import updateTasksVersion from '../store/tasks/updateTasksVersion';
import updateCharacterVersion from '../store/user/updateCharacterVersion';
import updateUnlocksVersion from '../store/unlocks/updateUnlocksVersion';
export default function useAccount({ redirectReturnToUrl }) { export default function useAccount() {
const { const [authModalOpen, setAuthModalOpen] = useState(false);
isLoading, const isLoggedIn = useSelector(state => state.account.accountCache.isLoggedIn);
isAuthenticated, const isChecking = useSelector(state => state.account.accountCache.isChecking);
user, const username = useSelector(state => state.account.accountCache.username);
loginWithRedirect, const userEmail = useSelector(state => state.account.accountCache.userEmail);
logout: logoutWithRedirect,
getAccessTokenSilently,
} = useAuth0();
const isLoginCache = useSelector(state => state.account.accountCache.isLoggedIn);
const accessToken = useSelector(state => state.account.accountCache.accessToken);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
if (isAuthenticated) { getAuthStatus().then(result => {
getAccessTokenSilently().then(token => { if (result.success && result.value?.authenticated) {
dispatch(updateAccountCache({ isAuthenticated, user, accessToken: token })); getCurrentUser().then(userResult => {
}); if (userResult.success) {
dispatch(setLoggedIn({
username: userResult.value.username,
email: userResult.value.email,
}));
} else { } else {
updateAccountCache({ isAuthenticated, user, accessToken: undefined }); dispatch(setLoggedOut());
} }
}, [isAuthenticated]);
const isLoggedIn = isLoginCache || isAuthenticated;
useEffect(() => {
if (isLoginCache && accessToken) {
getUser(user.email, accessToken).then(res => {
if (res.success) {
// user exists already, load their data and overwrite existing
batch(() => {
const settingsState = res.value.settings?.S ? JSON.parse(res.value.settings?.S) : INITIAL_SETTINGS_STATE;
const characterState = updateCharacterVersion(
res.value.character?.S ? JSON.parse(res.value.character?.S) : INITIAL_CHARACTER_STATE
);
const activeCharacter = characterState.characters[characterState.activeCharacter] ?? 'DEFAULT';
const taskState = updateTasksVersion(
res.value[`tasks_${activeCharacter}`]?.S
? JSON.parse(res.value[`tasks_${activeCharacter}`].S)
: INITIAL_TASKS_STATE
);
const unlocksState = updateUnlocksVersion(
res.value[`unlocks_${activeCharacter}`]?.S
? JSON.parse(res.value[`unlocks_${activeCharacter}`].S)
: INITIAL_UNLOCKS_STATE
);
dispatch(loadTasksState({ forceOverwrite: true, newState: taskState, skipDbUpdate: true }));
dispatch(loadSettingsState({ forceOverwrite: true, newState: settingsState, skipDbUpdate: true }));
dispatch(loadUnlocksState({ forceOverwrite: true, newState: unlocksState, skipDbUpdate: true }));
dispatch(loadCharacterState({ forceOverwrite: true, newState: characterState, skipDbUpdate: true }));
dispatch(fetchHiscores(characterState, null, true));
}); });
} else { } else {
// user does not exist, create them and reload the current local state to save it to the DB dispatch(setLoggedOut());
createUserIfNeeded(user.email, accessToken).then(result => { }
});
}, [dispatch]);
const handleAuthSuccess = userData => {
dispatch(setLoggedIn({
username: userData.username,
email: userData.email,
}));
};
const openAuthModal = () => {
setAuthModalOpen(true);
};
const closeAuthModal = () => {
setAuthModalOpen(false);
};
const logout = async () => {
const result = await logoutApi();
if (result.success) { if (result.success) {
batch(() => { dispatch(setLoggedOut());
dispatch(loadTasksState({ forceOverwrite: true, newState: loadTasksLocalState() }));
dispatch(loadSettingsState({ forceOverwrite: true, newState: loadSettingsLocalState() }));
dispatch(loadUnlocksState({ forceOverwrite: true, newState: loadUnlocksLocalState() }));
dispatch(loadCharacterState({ forceOverwrite: true, newState: loadCharacterLocalState() }));
});
}
});
}
});
}
}, [isLoginCache, accessToken]);
const isAuthenticating = isLoading;
const login = () => {
loginWithRedirect({ redirect_uri: redirectReturnToUrl });
};
const logout = () => {
logoutWithRedirect({ returnTo: redirectReturnToUrl });
resetState(dispatch, true); resetState(dispatch, true);
}
}; };
return { isLoggedIn, isAuthenticating, login, logout }; return {
isLoggedIn,
isAuthenticating: isChecking,
username,
userEmail,
authModalOpen,
openAuthModal,
closeAuthModal,
handleAuthSuccess,
logout,
};
} }

View File

@@ -2,14 +2,15 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
export const CURRENT_VERSION = 3; export const CURRENT_VERSION = 4;
const INITIAL_STATE = { const INITIAL_STATE = {
version: CURRENT_VERSION, version: CURRENT_VERSION,
accountCache: { accountCache: {
isLoggedIn: false, isLoggedIn: false,
isChecking: true,
username: undefined,
userEmail: undefined, userEmail: undefined,
accessToken: undefined,
}, },
}; };
@@ -17,18 +18,28 @@ export const accountSlice = createSlice({
name: 'account', name: 'account',
initialState: INITIAL_STATE, initialState: INITIAL_STATE,
reducers: { reducers: {
updateAccountCache: (state, action) => { setAuthChecking: (state, action) => {
state.accountCache.isLoggedIn = action.payload.isAuthenticated; state.accountCache.isChecking = action.payload;
state.accountCache.userEmail = action.payload.isAuthenticated ? action.payload.user.email : undefined; },
state.accountCache.accessToken = action.payload.isAuthenticated ? action.payload.accessToken : undefined; setLoggedIn: (state, action) => {
state.accountCache.isLoggedIn = true;
state.accountCache.isChecking = false;
state.accountCache.username = action.payload.username;
state.accountCache.userEmail = action.payload.email;
},
setLoggedOut: state => {
state.accountCache.isLoggedIn = false;
state.accountCache.isChecking = false;
state.accountCache.username = undefined;
state.accountCache.userEmail = undefined;
}, },
reset: () => INITIAL_STATE, reset: () => INITIAL_STATE,
}, },
}); });
// Don't cache anything across sessions, let auth0 handle it // Don't cache anything across sessions, session cookie handles it
export const loadState = () => INITIAL_STATE; export const loadState = () => INITIAL_STATE;
export const { updateAccountCache, reset } = accountSlice.actions; export const { setAuthChecking, setLoggedIn, setLoggedOut, reset } = accountSlice.actions;
export default accountSlice.reducer; export default accountSlice.reducer;

View File

@@ -3528,6 +3528,9 @@ select[multiple]:focus option:checked {
.font-medium { .font-medium {
font-weight: 500; font-weight: 500;
} }
.font-normal {
font-weight: 400;
}
.font-semibold { .font-semibold {
font-weight: 600; font-weight: 600;
} }

View File

@@ -1,5 +1,5 @@
[Unit] [Unit]
Description=OS League Tools - OSRS Leagues Hub Description=OS League Tools - Prod Environment
After=network.target After=network.target
Wants=network-online.target Wants=network-online.target
@@ -13,7 +13,7 @@ WorkingDirectory=/home/sonder/leagues-tools/os-league-tools-master
Environment="NODE_ENV=production" Environment="NODE_ENV=production"
Environment="PORT=3000" Environment="PORT=3000"
# Uncomment and set if you have a backend API: # Uncomment and set if you have a backend API:
# Environment="REACT_APP_RELDO_URL=http://localhost:8080" Environment="REACT_APP_RELDO_URL=https://api.leagues.tools"
# Start the application (serves the pre-built static files) # Start the application (serves the pre-built static files)
ExecStart=/usr/bin/serve -s build -l tcp://0.0.0.0:3000 ExecStart=/usr/bin/serve -s build -l tcp://0.0.0.0:3000