diff --git a/README.md b/README.md index b958804..394fb94 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This app is no longer a static demo. It now uses the backend API for authenticat - Added a shared API client with JWT handling, refresh-token retry, and normalized error messages. - Added shared auth/session state with `localStorage` persistence. - Updated the dashboard to support login and live inventory summary data. +- Added a site-admin user creation form on the Admin page backed by `POST /api/auth/register`. - Added a dedicated inventory management page for CRUD operations. - Updated search to call the backend search endpoints. - Updated barcode scanning so scans look up matching inventory items through the API. @@ -20,7 +21,7 @@ This app is no longer a static demo. It now uses the backend API for authenticat | Route | Purpose | | --- | --- | | `/` | Dashboard. Shows login when signed out, or inventory summary, chart, and nearest expiry data when signed in. | -| `/admin` | Household administration page for listing, creating, editing, inviting, and leaving households. | +| `/admin` | Site-admin page for household administration and user creation. | | `/inventory` | Manage locations and inventory items. | | `/search` | Search items and locations, then filter results further in the UI. | | `/barcode` | Scan a barcode and search the inventory API for matches. | @@ -68,10 +69,15 @@ Households: - `POST /api/households/{id}/invite` - `DELETE /api/households/{id}/leave` +Users: + +- `POST /api/auth/register` + Notes: -- Household creation is limited to users with the backend `Admin` role. -- Household editing and member invites are available when the current user is a household admin or site admin. +- The `/admin` route is only visible and accessible for users with the backend `Admin` role. +- User creation on this page uses the register contract: `email`, `password`, `confirmPassword`, `firstName`, and `lastName`. +- The register endpoint does not assign roles, so new users are created without admin access by default. ### Search (`src/pages/SearchPage/SearchPage.jsx`) @@ -114,8 +120,7 @@ The shared API client also supports: Notes: -- `register` is still implemented in the client helper, but there is currently no registration form in the UI. -- If you need to create a user, use the backend Swagger UI or another client to call `POST /api/auth/register`. +- `register` is exposed in the Admin page for site admins and does not replace the current signed-in session. ## API Configuration @@ -207,7 +212,7 @@ npm run preview 2. Enter an existing user email and password 3. Sign in to unlock the protected routes and API-backed data -There is no registration form in the UI at the moment. +There is no public registration form in the signed-out UI. Site admins can create accounts from `/admin`. ### Inventory Page @@ -226,11 +231,11 @@ Important update behavior: Use `/admin` to: -- Review all households returned for the current user -- Create a new household when the signed-in user has the site admin role -- Edit households the signed-in user administers +- Review household data as a site admin +- Create a new household - Invite members by email to the selected household - Leave a household from the same page +- Create a new user account through the register endpoint ### Search Page diff --git a/index.html b/index.html index 7616fda..ecd23fe 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Webpage Playground + Pantry Manager
diff --git a/src/App.jsx b/src/App.jsx index 34d42a9..4d3df7a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Routes, Route } from 'react-router-dom' +import { Navigate, Routes, Route } from 'react-router-dom' import Navbar from './components/Navbar/Navbar.jsx' import AdminPage from './pages/AdminPage/AdminPage.jsx' import HomePage from './pages/HomePage/HomePage.jsx' @@ -36,7 +36,7 @@ if (typeof document !== 'undefined') { } function App() { - const { isAuthenticated } = useAuth() + const { isAuthenticated, isSiteAdmin } = useAuth() const [theme, setTheme] = useState(initialTheme) useEffect(() => { @@ -61,7 +61,7 @@ function App() { {isAuthenticated ? ( } /> - } /> + : } /> } /> } /> } /> diff --git a/src/components/Navbar/Navbar.jsx b/src/components/Navbar/Navbar.jsx index 1845033..e8eced8 100644 --- a/src/components/Navbar/Navbar.jsx +++ b/src/components/Navbar/Navbar.jsx @@ -83,7 +83,7 @@ function Navbar({ theme, onToggleTheme }) { {isAuthenticated ? ( <>
  • Homepage
  • -
  • Admin
  • + {isSiteAdmin &&
  • Admin
  • }
  • Inventory
  • Search
  • Barcode Scanner
  • diff --git a/src/pages/AdminPage/AdminPage.css b/src/pages/AdminPage/AdminPage.css index 9e24e18..bcada34 100644 --- a/src/pages/AdminPage/AdminPage.css +++ b/src/pages/AdminPage/AdminPage.css @@ -78,6 +78,21 @@ min-width: 0; } +.admin-created-user-card { + margin-top: 20px; + padding: 16px; + border-radius: 12px; + background: var(--color-surface-muted); + border: 1px solid var(--color-border-muted); + display: grid; + gap: 12px; +} + +.admin-created-user-copy { + display: grid; + gap: 6px; +} + @media (max-width: 700px) { .member-row { flex-direction: column; diff --git a/src/pages/AdminPage/AdminPage.jsx b/src/pages/AdminPage/AdminPage.jsx index a064297..f01224f 100644 --- a/src/pages/AdminPage/AdminPage.jsx +++ b/src/pages/AdminPage/AdminPage.jsx @@ -9,16 +9,24 @@ const EMPTY_HOUSEHOLD_FORM = { description: '', } -function formatMemberName(member) { - const fullName = [member.firstName, member.lastName] +const EMPTY_USER_FORM = { + firstName: '', + lastName: '', + email: '', + password: '', + confirmPassword: '', +} + +function formatPersonName(person) { + const fullName = [person.firstName, person.lastName] .filter(Boolean) .join(' ') - return fullName || member.email || 'Unnamed member' + return fullName || person.email || 'Unnamed person' } function AdminPage() { - const { isAuthenticated, isSiteAdmin } = useAuth() + const { isAuthenticated, isSiteAdmin, register } = useAuth() const [loading, setLoading] = useState(false) const [errorMessage, setErrorMessage] = useState('') const [statusMessage, setStatusMessage] = useState('') @@ -27,6 +35,8 @@ function AdminPage() { const [editingHouseholdId, setEditingHouseholdId] = useState('') const [householdForm, setHouseholdForm] = useState(EMPTY_HOUSEHOLD_FORM) const [inviteEmail, setInviteEmail] = useState('') + const [userForm, setUserForm] = useState(EMPTY_USER_FORM) + const [createdUser, setCreatedUser] = useState(null) async function loadHouseholds(preferredSelectionId = '') { const response = await householdsApi.getHouseholds() @@ -59,6 +69,8 @@ function AdminPage() { setEditingHouseholdId('') setHouseholdForm(EMPTY_HOUSEHOLD_FORM) setInviteEmail('') + setUserForm(EMPTY_USER_FORM) + setCreatedUser(null) setErrorMessage('') setStatusMessage('') return @@ -99,12 +111,17 @@ function AdminPage() { const canSubmitHouseholdForm = editingHouseholdId ? Boolean(editingHousehold && (isSiteAdmin || editingHousehold.isCurrentUserHouseholdAdmin)) : isSiteAdmin + const createdUserRoles = Array.isArray(createdUser?.roles) ? createdUser.roles : [] function resetHouseholdEditor() { setEditingHouseholdId('') setHouseholdForm(EMPTY_HOUSEHOLD_FORM) } + function resetUserForm() { + setUserForm(EMPTY_USER_FORM) + } + async function handleHouseholdSubmit(event) { event.preventDefault() @@ -182,6 +199,54 @@ function AdminPage() { } } + async function handleUserSubmit(event) { + event.preventDefault() + + const email = userForm.email.trim() + + if (!isSiteAdmin) { + setErrorMessage('Only site admins can create users from the admin page.') + return + } + + if (!email) { + setErrorMessage('User email is required.') + return + } + + if (!userForm.password) { + setErrorMessage('A password is required to create a user.') + return + } + + if (userForm.password !== userForm.confirmPassword) { + setErrorMessage('Passwords do not match.') + return + } + + setLoading(true) + setErrorMessage('') + setStatusMessage('') + + try { + const result = await register({ + email, + password: userForm.password, + confirmPassword: userForm.confirmPassword, + firstName: userForm.firstName.trim() || null, + lastName: userForm.lastName.trim() || null, + }) + + setCreatedUser(result?.user ?? null) + resetUserForm() + setStatusMessage(result?.message || 'User created.') + } catch (error) { + setErrorMessage(error.message) + } finally { + setLoading(false) + } + } + async function handleLeaveHousehold(householdId) { const confirmed = window.confirm('Leave this household?') @@ -221,7 +286,7 @@ function AdminPage() { <> {errorMessage &&
    {errorMessage}
    } {statusMessage &&
    {statusMessage}
    } - {loading &&
    Syncing household data...
    } + {loading &&
    Processing admin request...
    }

    Household Summary

    @@ -335,6 +400,117 @@ function AdminPage() { )}
    + +
    +
    +

    Create User

    + {createdUser?.email || 'Register endpoint'} +
    + +

    + {isSiteAdmin + ? 'This creates a new account with the register endpoint. Role assignment still happens separately in the backend.' + : 'Only site admins can create users from this page.'} +

    + +
    +
    +
    + + setUserForm(current => ({ ...current, firstName: event.target.value }))} + placeholder="Jordan" + disabled={!isSiteAdmin} + /> +
    + +
    + + setUserForm(current => ({ ...current, lastName: event.target.value }))} + placeholder="Lee" + disabled={!isSiteAdmin} + /> +
    +
    + +
    + + setUserForm(current => ({ ...current, email: event.target.value }))} + placeholder="new-user@example.com" + disabled={!isSiteAdmin} + required + /> +
    + +
    +
    + + setUserForm(current => ({ ...current, password: event.target.value }))} + placeholder="Create a password" + disabled={!isSiteAdmin} + required + /> +
    + +
    + + setUserForm(current => ({ ...current, confirmPassword: event.target.value }))} + placeholder="Repeat the password" + disabled={!isSiteAdmin} + required + /> +
    +
    + +
    + + +
    +
    + + {createdUser && ( +
    +
    + {formatPersonName(createdUser)} +
    {createdUser.email || 'No email provided.'}
    +
    ID: {createdUser.id || 'Not available'}
    +
    + +
    + {createdUserRoles.length === 0 ? ( + No roles assigned + ) : ( + createdUserRoles.map(role => ( + {role} + )) + )} +
    +
    + )} +
    @@ -371,7 +547,7 @@ function AdminPage() { {(household.members ?? []).map(member => (
    - {formatMemberName(member)} + {formatPersonName(member)} {member.email}
    {member.isHouseholdAdmin && Admin}