Implement authentication features with login, registration, and user management; update service configurations for development and production environments.
This commit is contained in:
@@ -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
|
||||
|
||||
74
os-league-tools-master/src/client/auth-client.js
Normal file
74
os-league-tools-master/src/client/auth-client.js
Normal 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);
|
||||
}
|
||||
255
os-league-tools-master/src/components/AuthModal.js
Normal file
255
os-league-tools-master/src/components/AuthModal.js
Normal 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'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>
|
||||
);
|
||||
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3528,6 +3528,9 @@ select[multiple]:focus option:checked {
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user