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.'}
+
+
+
+
+ {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}