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_HOST=dev.leagues.tools"
Environment="WDS_SOCKET_PORT=443"
Environment="REACT_APP_RELDO_URL=https://api.leagues.tools"
# Start the dev server with hot reload
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 images from '../assets/images';
import useQueryString from '../hooks/useQueryString';
// import ManageData from './nav/ManageData';
import Feedback from './nav/Feedback';
// import Character from './nav/Character';
import AuthButton from './nav/AuthButton';
import Character from './nav/Character';
import ManageCharactersModal from './ManageCharactersModal';
export default function PageWrapper({ children }) {
@@ -18,9 +18,6 @@ export default function PageWrapper({ children }) {
const [isFeedbackModalOpen, setFeedbackModalOpen] = useState(false);
const [isCharacterModalOpen, setCharacterModalOpen] = useState(false);
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 = [
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('User', 'footer', 10, 1).withCustomRenderFn(
(isCollapsed, onNavigate) => (
<div
key='footer-user'
className='sidebar-nav-link'
onClick={() => {
// 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(
(isCollapsed, onNavigate) => (
<button
key='footer-selected-character'
type='button'
className='sidebar-nav-link w-full text-left'
onClick={() => {
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('Account', 'footer', 10, 0).withCustomRenderFn(
(isCollapsed, onNavigate) => (
<AuthButton.SideBarItem
key='auth'
isCollapsed={isCollapsed}
onNavigate={onNavigate}
/>
)
),
new NavItem('Character', 'footer', 10, 1).withCustomRenderFn(
(isCollapsed, onNavigate) => (
<Character.SideBarItem
key='character'
setCharacterModalOpen={setCharacterModalOpen}
isCollapsed={isCollapsed}
onNavigate={onNavigate}
/>
)
),
// new NavItem('Tip Jar', 'overflow', 4, 3)
// .withHref('https://ko-fi.com/osleaguetools', '_blank')
// .withIconFont('savings'),

View File

@@ -1,9 +1,26 @@
import React from 'react';
import React, { useRef, useState } from 'react';
import Spinner from '../common/Spinner';
import Dropdown from '../common/Dropdown';
import useAccount from '../../hooks/useAccount';
import useClickListener from '../../hooks/useClickListener';
import AuthModal from '../AuthModal';
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) {
return (
@@ -14,41 +31,198 @@ function NavBarItem() {
);
}
const { label, icon, action } = getButtonValues(isLoggedIn, login, logout);
if (!isLoggedIn) {
return (
<>
<button
className='text-primary md:inline hidden navbar-link-alt bg-hover px-2 py-1'
onClick={openAuthModal}
type='button'
>
<span className='icon-base mr-1 align-bottom'>login</span>
Login
</button>
<button className='md:hidden inline navbar-icon-link' onClick={openAuthModal} type='button'>
<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 (
<>
<button
className='text-primary md:inline hidden navbar-link-alt bg-hover px-2 py-1'
onClick={() => action()}
type='button'
>
{label}
</button>
<button className='md:hidden inline navbar-icon-link' onClick={action} type='button'>
<span className='text-primary-alt icon-lg leading-tight align-middle'>{icon}</span>
<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>
</>
);
}
function CollapsedMenu() {
const { isLoggedIn, login, logout } = useAccount({ redirectReturnToUrl: window.location.origin });
const { label, icon, action } = getButtonValues(isLoggedIn, login, logout);
function SideBarItem({ isCollapsed, onNavigate }) {
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) {
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 (
<button className='text-primary bg-hover py-1 text-left' onClick={() => action()} type='button'>
<span className='text-primary-alt icon-lg inline align-middle mr-1'>{icon}</span>
<p className='h-4 inline pl-1 font-sans-alt'>{label}</p>
</button>
<div className='relative' ref={menuRef}>
<button
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>
{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) {
return {
label: isLoggedIn ? 'Logout' : 'Login',
icon: isLoggedIn ? 'logout' : 'login',
action: isLoggedIn ? logout : login,
};
}
export default { NavBarItem, CollapsedMenu };
export default { NavBarItem, CollapsedMenu, SideBarItem };

View File

@@ -1,101 +1,68 @@
import { useAuth0 } from '@auth0/auth0-react';
import { useEffect } from 'react';
import { useDispatch, useSelector, batch } from 'react-redux';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { resetState } from '../store/common';
import { load as loadSettingsState, loadState as loadSettingsLocalState } from '../store/settings/settings';
import { load as loadTasksState, loadState as loadTasksLocalState } from '../store/tasks/tasks';
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';
import { setLoggedIn, setLoggedOut } from '../store/user/account';
import { getAuthStatus, getCurrentUser, logout as logoutApi } from '../client/auth-client';
export default function useAccount({ redirectReturnToUrl }) {
const {
isLoading,
isAuthenticated,
user,
loginWithRedirect,
logout: logoutWithRedirect,
getAccessTokenSilently,
} = useAuth0();
const isLoginCache = useSelector(state => state.account.accountCache.isLoggedIn);
const accessToken = useSelector(state => state.account.accountCache.accessToken);
export default function useAccount() {
const [authModalOpen, setAuthModalOpen] = useState(false);
const isLoggedIn = useSelector(state => state.account.accountCache.isLoggedIn);
const isChecking = useSelector(state => state.account.accountCache.isChecking);
const username = useSelector(state => state.account.accountCache.username);
const userEmail = useSelector(state => state.account.accountCache.userEmail);
const dispatch = useDispatch();
useEffect(() => {
if (isAuthenticated) {
getAccessTokenSilently().then(token => {
dispatch(updateAccountCache({ isAuthenticated, user, accessToken: token }));
});
} else {
updateAccountCache({ isAuthenticated, user, accessToken: undefined });
}
}, [isAuthenticated]);
getAuthStatus().then(result => {
if (result.success && result.value?.authenticated) {
getCurrentUser().then(userResult => {
if (userResult.success) {
dispatch(setLoggedIn({
username: userResult.value.username,
email: userResult.value.email,
}));
} else {
dispatch(setLoggedOut());
}
});
} else {
dispatch(setLoggedOut());
}
});
}, [dispatch]);
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 {
// user does not exist, create them and reload the current local state to save it to the DB
createUserIfNeeded(user.email, accessToken).then(result => {
if (result.success) {
batch(() => {
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);
const handleAuthSuccess = userData => {
dispatch(setLoggedIn({
username: userData.username,
email: userData.email,
}));
};
return { isLoggedIn, isAuthenticating, login, logout };
const openAuthModal = () => {
setAuthModalOpen(true);
};
const closeAuthModal = () => {
setAuthModalOpen(false);
};
const logout = async () => {
const result = await logoutApi();
if (result.success) {
dispatch(setLoggedOut());
resetState(dispatch, true);
}
};
return {
isLoggedIn,
isAuthenticating: isChecking,
username,
userEmail,
authModalOpen,
openAuthModal,
closeAuthModal,
handleAuthSuccess,
logout,
};
}

View File

@@ -2,14 +2,15 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
export const CURRENT_VERSION = 3;
export const CURRENT_VERSION = 4;
const INITIAL_STATE = {
version: CURRENT_VERSION,
accountCache: {
isLoggedIn: false,
isChecking: true,
username: undefined,
userEmail: undefined,
accessToken: undefined,
},
};
@@ -17,18 +18,28 @@ export const accountSlice = createSlice({
name: 'account',
initialState: INITIAL_STATE,
reducers: {
updateAccountCache: (state, action) => {
state.accountCache.isLoggedIn = action.payload.isAuthenticated;
state.accountCache.userEmail = action.payload.isAuthenticated ? action.payload.user.email : undefined;
state.accountCache.accessToken = action.payload.isAuthenticated ? action.payload.accessToken : undefined;
setAuthChecking: (state, action) => {
state.accountCache.isChecking = action.payload;
},
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,
},
});
// 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 { updateAccountCache, reset } = accountSlice.actions;
export const { setAuthChecking, setLoggedIn, setLoggedOut, reset } = accountSlice.actions;
export default accountSlice.reducer;

View File

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

View File

@@ -1,5 +1,5 @@
[Unit]
Description=OS League Tools - OSRS Leagues Hub
Description=OS League Tools - Prod Environment
After=network.target
Wants=network-online.target
@@ -13,7 +13,7 @@ WorkingDirectory=/home/sonder/leagues-tools/os-league-tools-master
Environment="NODE_ENV=production"
Environment="PORT=3000"
# 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)
ExecStart=/usr/bin/serve -s build -l tcp://0.0.0.0:3000