From 60f62df55ac5755e869f64fce6d2a1e3d7d44959 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 1 Jun 2025 23:32:02 +0100 Subject: [PATCH] Added better app loading and error handling --- Jenkinsfile | 4 +- src/App.jsx | 20 +++--- src/components/App/AppError.jsx | 65 +++++++++++++++++++ src/components/App/AppLoading.jsx | 33 ++++++++++ .../AppParticles.jsx} | 6 +- src/components/Auth/Auth.css | 29 --------- src/components/Auth/AuthLayout.jsx | 38 ----------- src/components/Auth/LoginUser.jsx | 52 --------------- src/components/Auth/_RegisterPasskey.jsx.old | 56 ---------------- .../Dashboard/Inventory/FilamentStocks.jsx | 2 +- .../Dashboard/Inventory/PartStocks.jsx | 2 +- .../Dashboard/Inventory/StockAudits.jsx | 2 +- .../Inventory/StockAudits/StockAuditInfo.jsx | 2 +- .../Dashboard/Inventory/StockEvents.jsx | 2 +- .../Dashboard/Management/Filaments.jsx | 2 +- .../Dashboard/Management/Materials.jsx | 2 +- src/components/Dashboard/Management/Parts.jsx | 2 +- .../Dashboard/Management/Products.jsx | 2 +- .../Management/Products/NewProduct.jsx | 2 +- .../Dashboard/Management/Vendors.jsx | 2 +- .../Dashboard/Production/GCodeFiles.jsx | 2 +- .../Production/GCodeFiles/NewGCodeFile.jsx | 2 +- .../Dashboard/Production/PrintJobs.jsx | 2 +- .../Dashboard/Production/Printers.jsx | 2 +- .../Production/Printers/ControlPrinter.jsx | 2 +- .../Dashboard/common/DashboardNavigation.jsx | 2 +- .../Dashboard/common/GCodeFileSelect.jsx | 2 +- .../Dashboard/common/PrinterSelect.jsx | 2 +- .../context}/AuthContext.js | 16 ++++- .../Dashboard/context/SocketContext.js | 2 +- src/components/PrivateRoute.jsx | 6 +- src/components/PublicRoute.jsx | 5 +- 32 files changed, 153 insertions(+), 217 deletions(-) create mode 100644 src/components/App/AppError.jsx create mode 100644 src/components/App/AppLoading.jsx rename src/components/{Auth/AuthParticles.jsx => App/AppParticles.jsx} (96%) delete mode 100644 src/components/Auth/Auth.css delete mode 100644 src/components/Auth/AuthLayout.jsx delete mode 100644 src/components/Auth/LoginUser.jsx delete mode 100644 src/components/Auth/_RegisterPasskey.jsx.old rename src/components/{Auth => Dashboard/context}/AuthContext.js (93%) diff --git a/Jenkinsfile b/Jenkinsfile index 4652068..cea4b98 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -39,10 +39,10 @@ node { remote.allowAnyHosts = true // Copy the build directory to the remote server - sshPut remote: remote, from: 'build/*', into: '/srv/farmcontrol-server/' + sshPut remote: remote, from: 'build/*', into: '/srv/farmcontrol-ui/' // Restart the service using sudo - sshCommand remote: remote, command: 'sudo /bin/systemctl restart farmcontrol-server.service' + sshCommand remote: remote, command: 'sudo /bin/systemctl restart nginx.service' } echo 'Pipeline completed successfully!' diff --git a/src/App.jsx b/src/App.jsx index 3d70f51..549170d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,7 +6,7 @@ import { Navigate } from 'react-router-dom' import { App, ConfigProvider } from 'antd' -import AuthLayout from './components/Auth/AuthLayout.jsx' + import ProductionOverview from './components/Dashboard/Production/ProductionOverview' import Printers from './components/Dashboard/Production/Printers' @@ -43,10 +43,9 @@ import StockAuditInfo from './components/Dashboard/Inventory/StockAudits/StockAu import Dashboard from './components/Dashboard/common/Dashboard' import PrivateRoute from './components/PrivateRoute' -import PublicRoute from './components/PublicRoute.jsx' import './App.css' import { SocketProvider } from './components/Dashboard/context/SocketContext.js' -import { AuthProvider } from './components/Auth/AuthContext.js' +import { AuthProvider } from './components/Dashboard/context/AuthContext.js' import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.js' import StockEvents from './components/Dashboard/Inventory/StockEvents.jsx' import Settings from './components/Dashboard/Management/Settings' @@ -54,6 +53,7 @@ import { ThemeProvider, useThemeContext } from './components/Dashboard/context/ThemeContext' +import AppError from './components/App/AppError' const AppContent = () => { const { themeConfig } = useThemeContext() @@ -79,11 +79,6 @@ const AppContent = () => { /> } /> - } />} - /> - } />} @@ -175,6 +170,15 @@ const AppContent = () => { /> } /> + + } + /> diff --git a/src/components/App/AppError.jsx b/src/components/App/AppError.jsx new file mode 100644 index 0000000..c54ec6d --- /dev/null +++ b/src/components/App/AppError.jsx @@ -0,0 +1,65 @@ +import React from 'react' +import { Flex, Card, Alert, Button } from 'antd' +import AuthParticles from './AppParticles' +import FarmControlLogo from '../Logos/FarmControlLogo' +import ExclamationOctagonIcon from '../Icons/ExclamationOctagonIcon' +import PropTypes from 'prop-types' +import ArrowLeftIcon from '../Icons/ArrowLeftIcon' +import ReloadIcon from '../Icons/ReloadIcon' + +const AppError = ({ + message = 'Error Message', + showBack = true, + showRefresh = true +}) => { + const handleBack = () => { + window.history.back() + } + + const handleRefresh = () => { + window.location.reload() + } + + return ( + <> + + + + + + + + } + type={'error'} + showIcon + /> + {(showBack || showRefresh) && ( + + {showBack && ( + - - - - - - - ) -} - -export default LoginUser diff --git a/src/components/Auth/_RegisterPasskey.jsx.old b/src/components/Auth/_RegisterPasskey.jsx.old deleted file mode 100644 index 0e51c14..0000000 --- a/src/components/Auth/_RegisterPasskey.jsx.old +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useState, useContext } from 'react' -import { useNavigate } from 'react-router-dom' -import { Button, Typography, Flex } from 'antd' -import { LockOutlined } from '@ant-design/icons' -import { AuthContext } from './AuthContext' - -import PassKeysIcon from '../Icons/PassKeysIcon' // Adjust the path if necessary - -import './Auth.css' -import AuthLayout from './AuthLayout' - -const { Text } = Typography - -const RegisterPasskey = () => { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState('') - const navigate = useNavigate() - const { registerPasskey } = useContext(AuthContext) - const [init, setInit] = useState(false) - - const handleRegisterPasskey = async (e) => { - const result = await registerPasskey(email, password) - if (result.successful === true) { - setTimeout(() => { - navigate('/dashboard/overview') - }, 500) - } else { - } - } - - return ( - - - -

Register a Passkey

- - Please setup a passkey in order to continue. The passkey may use - another device for encryption. - -
- -
- ) -} - -export default RegisterPasskey diff --git a/src/components/Dashboard/Inventory/FilamentStocks.jsx b/src/components/Dashboard/Inventory/FilamentStocks.jsx index 459f2ae..600ea1f 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks.jsx @@ -16,7 +16,7 @@ import { import { createStyles } from 'antd-style' import { LoadingOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import { SocketContext } from '../context/SocketContext' import NewFilamentStock from './FilamentStocks/NewFilamentStock' diff --git a/src/components/Dashboard/Inventory/PartStocks.jsx b/src/components/Dashboard/Inventory/PartStocks.jsx index 53910ca..49086e9 100644 --- a/src/components/Dashboard/Inventory/PartStocks.jsx +++ b/src/components/Dashboard/Inventory/PartStocks.jsx @@ -16,7 +16,7 @@ import { import { createStyles } from 'antd-style' import { LoadingOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import NewPartStock from './PartStocks/NewPartStock' import IdText from '../common/IdText' diff --git a/src/components/Dashboard/Inventory/StockAudits.jsx b/src/components/Dashboard/Inventory/StockAudits.jsx index 3157aee..7ccc85c 100644 --- a/src/components/Dashboard/Inventory/StockAudits.jsx +++ b/src/components/Dashboard/Inventory/StockAudits.jsx @@ -6,7 +6,7 @@ import { Table, Button, Flex, Space, message, Dropdown, Typography } from 'antd' import { createStyles } from 'antd-style' import { LoadingOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import { SocketContext } from '../context/SocketContext' import IdText from '../common/IdText' diff --git a/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx b/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx index 3df95a6..12abd6d 100644 --- a/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx +++ b/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx @@ -17,7 +17,7 @@ import { ClockCircleOutlined } from '@ant-design/icons' -import { AuthContext } from '../../../Auth/AuthContext' +import { AuthContext } from '../../context/AuthContext' import IdText from '../../common/IdText' import TimeDisplay from '../../common/TimeDisplay' diff --git a/src/components/Dashboard/Inventory/StockEvents.jsx b/src/components/Dashboard/Inventory/StockEvents.jsx index a4745a1..59bd578 100644 --- a/src/components/Dashboard/Inventory/StockEvents.jsx +++ b/src/components/Dashboard/Inventory/StockEvents.jsx @@ -17,7 +17,7 @@ import { createStyles } from 'antd-style' import { LoadingOutlined, AuditOutlined } from '@ant-design/icons' import moment from 'moment' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import { SocketContext } from '../context/SocketContext' import IdText from '../common/IdText' import TimeDisplay from '../common/TimeDisplay' diff --git a/src/components/Dashboard/Management/Filaments.jsx b/src/components/Dashboard/Management/Filaments.jsx index ada2ba9..03dec51 100644 --- a/src/components/Dashboard/Management/Filaments.jsx +++ b/src/components/Dashboard/Management/Filaments.jsx @@ -21,7 +21,7 @@ import { import { createStyles } from 'antd-style' import { LoadingOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import NewFilament from './Filaments/NewFilament' import IdText from '../common/IdText' import FilamentIcon from '../../Icons/FilamentIcon' diff --git a/src/components/Dashboard/Management/Materials.jsx b/src/components/Dashboard/Management/Materials.jsx index b46ab4d..192e942 100644 --- a/src/components/Dashboard/Management/Materials.jsx +++ b/src/components/Dashboard/Management/Materials.jsx @@ -16,7 +16,7 @@ import { import { createStyles } from 'antd-style' import { LoadingOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import NewMaterial from './Materials/NewMaterial' import IdText from '../common/IdText' diff --git a/src/components/Dashboard/Management/Parts.jsx b/src/components/Dashboard/Management/Parts.jsx index 71fe49d..bbeb03a 100644 --- a/src/components/Dashboard/Management/Parts.jsx +++ b/src/components/Dashboard/Management/Parts.jsx @@ -21,7 +21,7 @@ import { import { createStyles } from 'antd-style' import { LoadingOutlined, DownloadOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import IdText from '../common/IdText' import NewProduct from './Products/NewProduct' import PartIcon from '../../Icons/PartIcon' diff --git a/src/components/Dashboard/Management/Products.jsx b/src/components/Dashboard/Management/Products.jsx index f73d2bb..750a453 100644 --- a/src/components/Dashboard/Management/Products.jsx +++ b/src/components/Dashboard/Management/Products.jsx @@ -20,7 +20,7 @@ import { import { createStyles } from 'antd-style' import { LoadingOutlined, DownloadOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import IdText from '../common/IdText' import TimeDisplay from '../common/TimeDisplay' diff --git a/src/components/Dashboard/Management/Products/NewProduct.jsx b/src/components/Dashboard/Management/Products/NewProduct.jsx index 71d7019..e289bc4 100644 --- a/src/components/Dashboard/Management/Products/NewProduct.jsx +++ b/src/components/Dashboard/Management/Products/NewProduct.jsx @@ -18,7 +18,7 @@ import { InputNumber } from 'antd' import { DeleteOutlined, EyeOutlined } from '@ant-design/icons' -import { AuthContext } from '../../../Auth/AuthContext' +import { AuthContext } from '../../context/AuthContext' import PartIcon from '../../../Icons/PartIcon' import { StlViewer } from 'react-stl-viewer' import VendorSelect from '../../common/VendorSelect' diff --git a/src/components/Dashboard/Management/Vendors.jsx b/src/components/Dashboard/Management/Vendors.jsx index 9663cb3..6641d60 100644 --- a/src/components/Dashboard/Management/Vendors.jsx +++ b/src/components/Dashboard/Management/Vendors.jsx @@ -17,7 +17,7 @@ import { } from 'antd' import { createStyles } from 'antd-style' import { LoadingOutlined, ExportOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import IdText from '../common/IdText' import NewVendor from './Vendors/NewVendor' import CountryDisplay from '../common/CountryDisplay' diff --git a/src/components/Dashboard/Production/GCodeFiles.jsx b/src/components/Dashboard/Production/GCodeFiles.jsx index c48b190..8380af7 100644 --- a/src/components/Dashboard/Production/GCodeFiles.jsx +++ b/src/components/Dashboard/Production/GCodeFiles.jsx @@ -22,7 +22,7 @@ import { import { createStyles } from 'antd-style' import { LoadingOutlined, DownloadOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import NewGCodeFile from './GCodeFiles/NewGCodeFile' import IdText from '../common/IdText' import GCodeFileIcon from '../../Icons/GCodeFileIcon' diff --git a/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx b/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx index 2f07c8e..5936a08 100644 --- a/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx +++ b/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx @@ -23,7 +23,7 @@ import { } from 'antd' import { LoadingOutlined } from '@ant-design/icons' -import { AuthContext } from '../../../Auth/AuthContext' +import { AuthContext } from '../../context/AuthContext.js' import GCodeFileIcon from '../../../Icons/GCodeFileIcon' diff --git a/src/components/Dashboard/Production/PrintJobs.jsx b/src/components/Dashboard/Production/PrintJobs.jsx index 5f6547b..dc3eb59 100644 --- a/src/components/Dashboard/Production/PrintJobs.jsx +++ b/src/components/Dashboard/Production/PrintJobs.jsx @@ -21,7 +21,7 @@ import { import { createStyles } from 'antd-style' import { LoadingOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext.js' import { SocketContext } from '../context/SocketContext' import NewPrintJob from './PrintJobs/NewPrintJob' import JobState from '../common/JobState' diff --git a/src/components/Dashboard/Production/Printers.jsx b/src/components/Dashboard/Production/Printers.jsx index 70edfd5..698bf8c 100644 --- a/src/components/Dashboard/Production/Printers.jsx +++ b/src/components/Dashboard/Production/Printers.jsx @@ -20,7 +20,7 @@ import { import { createStyles } from 'antd-style' import { LoadingOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import PrinterState from '../common/PrinterState' import NewPrinter from './Printers/NewPrinter' import IdText from '../common/IdText' diff --git a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx index 80db2a4..abe75f2 100644 --- a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx +++ b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx @@ -27,7 +27,7 @@ import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel' import PrinterPositionPanel from '../../common/PrinterPositionPanel' import PrinterMovementPanel from '../../common/PrinterMovementPanel' import PrinterState from '../../common/PrinterState' -import { AuthContext } from '../../../Auth/AuthContext' +import { AuthContext } from '../../context/AuthContext' import PrinterSubJobsTree from '../../common/PrinterJobsTree' import IdText from '../../common/IdText' diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx index 87015fe..77ed532 100644 --- a/src/components/Dashboard/common/DashboardNavigation.jsx +++ b/src/components/Dashboard/common/DashboardNavigation.jsx @@ -22,7 +22,7 @@ import { DisconnectOutlined, MenuOutlined } from '@ant-design/icons' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import { SocketContext } from '../context/SocketContext' import { SpotlightContext } from '../context/SpotlightContext' import { useNavigate, useLocation } from 'react-router-dom' diff --git a/src/components/Dashboard/common/GCodeFileSelect.jsx b/src/components/Dashboard/common/GCodeFileSelect.jsx index 07f9bda..cd6c082 100644 --- a/src/components/Dashboard/common/GCodeFileSelect.jsx +++ b/src/components/Dashboard/common/GCodeFileSelect.jsx @@ -4,7 +4,7 @@ import { TreeSelect, Badge, Flex, message, Typography } from 'antd' import React, { useEffect, useState, useContext } from 'react' import axios from 'axios' import GCodeFileIcon from '../../Icons/GCodeFileIcon' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import config from '../../../config' diff --git a/src/components/Dashboard/common/PrinterSelect.jsx b/src/components/Dashboard/common/PrinterSelect.jsx index 8a7cfc3..0dd5492 100644 --- a/src/components/Dashboard/common/PrinterSelect.jsx +++ b/src/components/Dashboard/common/PrinterSelect.jsx @@ -4,7 +4,7 @@ import { TreeSelect, message, Tag } from 'antd' import React, { useEffect, useState, useContext } from 'react' import axios from 'axios' import PrinterState from './PrinterState' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from '../context/AuthContext' import config from '../../../config' const PrinterSelect = ({ onChange, disabled, checkable, value = [] }) => { diff --git a/src/components/Auth/AuthContext.js b/src/components/Dashboard/context/AuthContext.js similarity index 93% rename from src/components/Auth/AuthContext.js rename to src/components/Dashboard/context/AuthContext.js index 502cd71..548765a 100644 --- a/src/components/Auth/AuthContext.js +++ b/src/components/Dashboard/context/AuthContext.js @@ -3,9 +3,10 @@ import React, { createContext, useState, useCallback, useEffect } from 'react' import axios from 'axios' import { message, Modal, notification, Progress, Button, Space } from 'antd' import PropTypes from 'prop-types' -import ExclamationOctogonIcon from '../Icons/ExclamationOctagonIcon' -import InfoCircleIcon from '../Icons/InfoCircleIcon' -import config from '../../config' +import ExclamationOctogonIcon from '../../Icons/ExclamationOctagonIcon' +import InfoCircleIcon from '../../Icons/InfoCircleIcon' +import config from '../../../config' +import AppError from '../../App/AppError' const AuthContext = createContext() @@ -20,6 +21,7 @@ const AuthProvider = ({ children }) => { const [userProfile, setUserProfile] = useState(null) const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false) const [showUnauthorizedModal, setShowUnauthorizedModal] = useState(false) + const [authError, setAuthError] = useState(null) const logout = useCallback((redirectUri = '/login') => { setAuthenticated(false) @@ -40,6 +42,7 @@ const AuthProvider = ({ children }) => { // Function to check if the user is logged in const checkAuthStatus = useCallback(async () => { setLoading(true) + setAuthError(null) try { // Make a call to your backend to check auth status const response = await axios.get(`${config.backendUrl}/auth/user`, { @@ -54,11 +57,14 @@ const AuthProvider = ({ children }) => { setUserProfile(response.data) } else { setAuthenticated(false) + setAuthError('Failed to authenticate user.') } } catch (error) { console.log('Auth check failed', error) if (error.response?.status === 401) { setShowUnauthorizedModal(true) + } else { + setAuthError('Error connecting to authentication service.') } setAuthenticated(false) } finally { @@ -179,6 +185,10 @@ const AuthProvider = ({ children }) => { checkAuthStatus() }, [checkAuthStatus]) + if (authError) { + return + } + return ( <> {contextHolder} diff --git a/src/components/Dashboard/context/SocketContext.js b/src/components/Dashboard/context/SocketContext.js index db74b98..e6c603d 100644 --- a/src/components/Dashboard/context/SocketContext.js +++ b/src/components/Dashboard/context/SocketContext.js @@ -9,7 +9,7 @@ import React, { import io from 'socket.io-client' import { message, notification } from 'antd' import PropTypes from 'prop-types' -import { AuthContext } from '../../Auth/AuthContext' +import { AuthContext } from './AuthContext' import config from '../../../config' const SocketContext = createContext() diff --git a/src/components/PrivateRoute.jsx b/src/components/PrivateRoute.jsx index 660837a..baad30b 100644 --- a/src/components/PrivateRoute.jsx +++ b/src/components/PrivateRoute.jsx @@ -1,8 +1,8 @@ // PrivateRoute.js import PropTypes from 'prop-types' import React, { useContext } from 'react' -//import { Navigate } from 'react-router-dom' -import { AuthContext } from './Auth/AuthContext' +import { AuthContext } from './Dashboard/context/AuthContext' +import AuthLoading from './App/AppLoading' const PrivateRoute = ({ component: Component }) => { const { authenticated, loading, showSessionExpiredModal } = @@ -10,7 +10,7 @@ const PrivateRoute = ({ component: Component }) => { // Show loading state while auth state is being determined if (loading) { - return
Loading...
+ return } // Redirect to login if not authenticated diff --git a/src/components/PublicRoute.jsx b/src/components/PublicRoute.jsx index 45013b6..a710135 100644 --- a/src/components/PublicRoute.jsx +++ b/src/components/PublicRoute.jsx @@ -2,14 +2,15 @@ import PropTypes from 'prop-types' import React, { useContext } from 'react' import { Navigate } from 'react-router-dom' -import { AuthContext } from './Auth/AuthContext' +import { AuthContext } from './Dashboard/context/AuthContext' +import AuthLoading from './App/AppLoading' const PublicRoute = ({ component: Component }) => { const { authenticated, loading } = useContext(AuthContext) // Show loading state while auth state is being determined if (loading) { - return
Loading...
+ return } // Redirect to login if not authenticated