Skip to content

Commit

Permalink
Includes significant refactoring and enhancements across various comp…
Browse files Browse the repository at this point in the history
…onents. It fixes issues with form validation, error handling, and authentication flows. The logout functionality is improved to ensure a smooth user experience. Additionally, custom hooks for authentication and persistence are added. Global styles are updated for a consistent UI. The server-side code is also refined for better error handling and functionality.
  • Loading branch information
absolute-xero7 committed Jun 10, 2024
1 parent cec7f3c commit 720fb14
Show file tree
Hide file tree
Showing 24 changed files with 692 additions and 117 deletions.
30 changes: 15 additions & 15 deletions UserStories.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# User Stories for the app

1. [ ] Replace current sticky note system
1. [x] Replace current sticky note system
2. [x] Add a public facing page with basic contact info
3. [ ] Add an employee login to the notes app
3. [x] Add an employee login to the notes app
4. [x] Provide a welcome page after login
5. [ ] Provide easy navigation
6. [ ] Display current user and assigned role
7. [ ] Provide a logout option
8. [ ] Require users to login at least once per week
9. [ ] Provide a way to remove employee access asap if needed
5. [x] Provide easy navigation
6. [x] Display current user and assigned role
7. [x] Provide a logout option
8. [x] Require users to login at least once per week
9. [x] Provide a way to remove employee access asap if needed
10. [x] Notes are assigned to specific employees
11. [ ] Notes have a ticket #, title, note body, created & updated dates
11. [x] Notes have a ticket #, title, note body, created & updated dates
12. [x] Notes are either OPEN or COMPLETED
13. [ ] Users can be Employees, Managers, or Admins
14. [ ] Notes can only be deleted by Managers or Admins
15. [ ] Anyone can create a note (when customer checks-in)
16. [ ] Employees can only view and edit their assigned notes
17. [ ] Managers and Admins can view, edit, and delete all notes
18. [ ] Only Managers and Admins can access User Settings
19. [ ] Only Managers and Admins can create new users
13. [x] Users can be Employees, Managers, or Admins
14. [x] Notes can only be deleted by Managers or Admins
15. [x] Anyone can create a note (when customer checks-in)
16. [x] Employees can only view and edit their assigned notes
17. [x] Managers and Admins can view, edit, and delete all notes
18. [x] Only Managers and Admins can access User Settings
19. [x] Only Managers and Admins can create new users
47 changes: 29 additions & 18 deletions client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,44 @@ import NewUserForm from "./features/users/NewUserForm";
import EditNote from "./features/notes/EditNote";
import NewNote from "./features/notes/NewNote";
import Prefetch from "./features/auth/Prefetch";
import PersistLogin from "./features/auth/PersistLogin";
import RequireAuth from "./features/auth/RequireAuth";
import { ROLES } from "./config/roles";

function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
{/* Public Routes */}
<Route index element={<Public />} />
<Route path="login" element={<Login />} />

<Route element={<Prefetch />}>
<Route path="dash" element={<DashLayout />}>

<Route index element={<Welcome />} />

<Route path="users">
<Route index element={<UsersList />} />
<Route path=":id" element={<EditUser />} />
<Route path="new" element={<NewUserForm />} />
{/* Protected Routes */}
<Route element={<PersistLogin />}>
<Route element={<RequireAuth allowedRoles={[...Object.values(ROLES)]} />}>
<Route element={<Prefetch />}>
<Route path="dash" element={<DashLayout />}>

<Route index element={<Welcome />} />

<Route element={<RequireAuth allowedRoles={[ROLES.Manager, ROLES.Admin]} />}>
<Route path="users">
<Route index element={<UsersList />} />
<Route path=":id" element={<EditUser />} />
<Route path="new" element={<NewUserForm />} />
</Route>
</Route>

<Route path="notes">
<Route index element={<NotesList />} />
<Route path=":id" element={<EditNote />} />
<Route path="new" element={<NewNote />} />
</Route>

</Route>{/* End Dash */}
</Route>

<Route path="notes">
<Route index element={<NotesList />} />
<Route path=":id" element={<EditNote />} />
<Route path="new" element={<NewNote />} />
</Route>

</Route>{/* End Dash */}
</Route>
</Route>
</Route>{/* End Protected Routes */}

</Route>
</Routes>
Expand Down
52 changes: 48 additions & 4 deletions client/src/app/api/apiSlice.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,55 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { setCredentials } from '../../features/auth/authSlice';

const baseQuery = fetchBaseQuery({
baseUrl: 'http://localhost:3500',
credentials: 'include',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token

if (token) {
headers.set("authorization", `Bearer ${token}`)
}
return headers
}
})

const baseQueryWithReauth = async (args, api, extraOptions) => {
// console.log(args) // request url, method, body
// console.log(api) // signal, dispatch, getState()
// console.log(extraOptions) //custom like {shout: true}

let result = await baseQuery(args, api, extraOptions);

// If you want, handle other status codes, too
if (result?.error?.status === 403) {
console.log('sending refresh token')

// send refresh token to get new access token
const refreshResult = await baseQuery('/auth/refresh', api, extraOptions);

if (refreshResult?.data) {

// store the new token
api.dispatch(setCredentials({ ...refreshResult.data }))

// retry original query with new access token
result = await baseQuery(args, api, extraOptions)
} else {

if (refreshResult?.error?.status === 403) {
refreshResult.error.data.message = "Your login has expired. "
}
return refreshResult
}
};

return result;
};

// Create an API slice using createApi
export const apiSlice = createApi({
// Specify the baseQuery configuration with the base URL
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:3500' }),
// Define tagTypes for caching and invalidation
baseQuery: baseQueryWithReauth,
tagTypes: ['Note', 'User'],
// Define endpoints using the builder function
endpoints: builder => ({})
});
2 changes: 2 additions & 0 deletions client/src/app/store.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { configureStore } from "@reduxjs/toolkit";
import { apiSlice } from './api/apiSlice';
import { setupListeners } from "@reduxjs/toolkit/query";
import authReducer from "../features/auth/authSlice";

export const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
auth: authReducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(apiSlice.middleware),
Expand Down
27 changes: 14 additions & 13 deletions client/src/components/DashFooter.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { faHouse } from "@fortawesome/free-solid-svg-icons";
// import { useNavigate, useLocation } from "react-router-dom";
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
// import { faHouse } from "@fortawesome/free-solid-svg-icons"
// import { useNavigate, useLocation } from 'react-router-dom'
import useAuth from "../hooks/useAuth"

const DashFooter = () => {

// const navigate = useNavigate();
// const { pathname } = useLocation();
const { username, status } = useAuth()

// const onGoHomeClicked = () => navigate("/dash");
// const navigate = useNavigate()
// const { pathname } = useLocation()

// let goHomeButton = null;
// if (pathname !== "/dash") {
// const onGoHomeClicked = () => navigate('/dash')

// let goHomeButton = null
// if (pathname !== '/dash') {
// goHomeButton = (
// <button
// className="dash-footer__button icon-button"
Expand All @@ -23,14 +26,12 @@ const DashFooter = () => {
// }

const content = (
<footer className='dash-footer'>
<footer className="dash-footer">
{/* {goHomeButton} */}
<p>Current User:</p>
<p>Status:</p>
<p>Current User: {username}</p>
<p>Status: {status}</p>
</footer>
)
return content

}

export default DashFooter
150 changes: 138 additions & 12 deletions client/src/components/DashHeader.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,147 @@
import { Link } from "react-router-dom";
import { useEffect } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faFileCirclePlus,
faFilePen,
faUserGear,
faUserPlus,
faRightFromBracket
} from "@fortawesome/free-solid-svg-icons"
import { useNavigate, Link, useLocation } from 'react-router-dom'

import { useSendLogoutMutation } from '../features/auth/authApiSlice'

import useAuth from '../hooks/useAuth'

const DASH_REGEX = /^\/dash(\/)?$/
const NOTES_REGEX = /^\/dash\/notes(\/)?$/
const USERS_REGEX = /^\/dash\/users(\/)?$/

const DashHeader = () => {
const { isManager, isAdmin } = useAuth()

const navigate = useNavigate()
const { pathname } = useLocation()

const [sendLogout, {
isLoading,
isSuccess,
isError,
error
}] = useSendLogoutMutation()

useEffect(() => {
if (isSuccess) navigate('/')
}, [isSuccess, navigate])

const onNewNoteClicked = () => navigate('/dash/notes/new')
const onNewUserClicked = () => navigate('/dash/users/new')
const onNotesClicked = () => navigate('/dash/notes')
const onUsersClicked = () => navigate('/dash/users')

let dashClass = null
if (!DASH_REGEX.test(pathname) && !NOTES_REGEX.test(pathname) && !USERS_REGEX.test(pathname)) {
dashClass = "dash-header__container--small"
}

let newNoteButton = null
if (NOTES_REGEX.test(pathname)) {
newNoteButton = (
<button
className="icon-button"
title="New Note"
onClick={onNewNoteClicked}
>
<FontAwesomeIcon icon={faFileCirclePlus} />
</button>
)
}

let newUserButton = null
if (USERS_REGEX.test(pathname)) {
newUserButton = (
<button
className="icon-button"
title="New User"
onClick={onNewUserClicked}
>
<FontAwesomeIcon icon={faUserPlus} />
</button>
)
}

let userButton = null
if (isManager || isAdmin) {
if (!USERS_REGEX.test(pathname) && pathname.includes('/dash')) {
userButton = (
<button
className="icon-button"
title="Users"
onClick={onUsersClicked}
>
<FontAwesomeIcon icon={faUserGear} />
</button>
)
}
}

let notesButton = null
if (!NOTES_REGEX.test(pathname) && pathname.includes('/dash')) {
notesButton = (
<button
className="icon-button"
title="Notes"
onClick={onNotesClicked}
>
<FontAwesomeIcon icon={faFilePen} />
</button>
)
}

const logoutButton = (
<button
className="icon-button"
title="Logout"
onClick={sendLogout}
>
<FontAwesomeIcon icon={faRightFromBracket} />
</button>
)

const errClass = isError ? "errmsg" : "offscreen"

let buttonContent
if (isLoading) {
buttonContent = <p>Logging Out...</p>
} else {
buttonContent = (
<>
{newNoteButton}
{newUserButton}
{notesButton}
{userButton}
{logoutButton}
</>
)
}

const content = (
<header className="dash-header">
<div className="dash-header__container">
<Link to="/dash">
<h1 className="dash-header__title">fixScribe</h1>
</Link>
<nav className="dash-header__nav">
{/* add nav buttons later */}
</nav>
</div>
</header>
<>
<p className={errClass}>{error?.data?.message}</p>

<header className="dash-header">
<div className={`dash-header__container ${dashClass}`}>
<Link to="/dash">
<h1 className="dash-header__title">techNotes</h1>
</Link>
<nav className="dash-header__nav">
{buttonContent}
</nav>
</div>
</header>
</>
)

return content
}

export default DashHeader
Loading

0 comments on commit 720fb14

Please sign in to comment.