# User Stories — CMS Frontend **Format**: "As a [persona], I want to [action], so that [benefit]" **Acceptance Criteria**: Detailed checklist — happy path, error states, edge cases, and security constraints --- ## Epic: Authentication & Session ### US-01: Login with email and password **Persona**: Owner, Admin, User As an Owner/Admin/User, I want to log in with my email address and password, so that I can access the CMS platform securely. **Acceptance Criteria**: - [ ] A `/login` page is displayed for unauthenticated users - [ ] The form contains an email field, a password field, and a submit button - [ ] Email field validates format before submission (invalid format shows inline error) - [ ] Password field enforces backend rules client-side: minimum 8 characters, at least 1 uppercase letter, 1 lowercase letter, 1 digit, and 1 non-alphanumeric character (e.g. `!@#$%`) - [ ] On valid credentials: access token is stored in memory, refresh token is set as httpOnly cookie, user is redirected to `/` - [ ] On invalid credentials: a generic error message is shown ("Invalid email or password") — no distinction between wrong email and wrong password - [ ] On network error: a user-friendly error message is shown ("Unable to connect. Please try again.") - [ ] The password field input is masked; a show/hide toggle is present - [ ] The submit button shows a loading state while the request is in progress - [ ] After successful login, the back button does NOT navigate back to the login page - [ ] The page is accessible when the system is initialized and the user is not logged in - [ ] **Security**: No token is stored in localStorage or sessionStorage - [ ] **Security**: The form does not autocomplete passwords in production (autocomplete="new-password" or "current-password" as appropriate) --- ### US-02: Stay logged in across tab switches (session persistence via refresh token) **Persona**: Owner, Admin, User As an authenticated user, I want my session to be restored when I return to the app after a page refresh or browser restart, so that I do not have to log in repeatedly. **Acceptance Criteria**: - [ ] On app load, the app calls `POST /auth/refresh` using the httpOnly cookie before rendering protected routes - [ ] If the refresh succeeds: the new access token is stored in memory and the user proceeds to their intended route - [ ] If the refresh fails (expired or missing cookie): the user is redirected to `/login` - [ ] During the session restoration check, a loading/spinner state is shown — no flash of the protected content or login page - [ ] A user who has never logged in sees the login page immediately (no loading delay) - [ ] **Security**: The refresh endpoint call uses credentials (cookies) — `credentials: 'include'` in fetch or equivalent --- ### US-03: Logout and end session **Persona**: Owner, Admin, User As an authenticated user, I want to log out of the application, so that my session is terminated and no one else can use my account. **Acceptance Criteria**: - [ ] A logout button/menu item is visible in the sidebar for all authenticated users - [ ] Clicking logout calls `POST /auth/revoke` with the current refresh token - [ ] After logout: in-memory access token is cleared, the browser is redirected to `/login` - [ ] If the revoke call fails (network error): the user is still redirected to `/login` and local state is cleared - [ ] After logout, navigating back (browser back button) to a protected page redirects to `/login` - [ ] **Security**: The httpOnly cookie is cleared upon logout (backend sets `Set-Cookie` with expired date) --- ### US-04: Redirect to login when not authenticated **Persona**: Anonymous Visitor As an unauthenticated visitor, I want to be redirected to the login page when I try to access a protected route, so that I cannot access content I am not authorised to see. **Acceptance Criteria**: - [ ] Navigating to any protected route (e.g. `/`, `/users`, `/settings`) without a valid session redirects to `/login` - [ ] After redirect, the originally requested URL is preserved (e.g. as a `?redirect=/users` query param) so the user can be sent there after login - [ ] No protected page content is rendered, even briefly, before the redirect occurs - [ ] **Security**: The redirect happens client-side as a defence-in-depth measure; the API also enforces authorisation server-side --- ### US-05: Automatic access token refresh on expiry **Persona**: Owner, Admin, User As an authenticated user, I want my access token to be refreshed automatically when it expires during an active session, so that I am not abruptly logged out while working. **Acceptance Criteria**: - [ ] When an API call returns a 401 (Unauthorised), the app automatically calls `POST /auth/refresh` - [ ] If the refresh succeeds: the original API call is retried with the new access token - [ ] If the refresh fails (refresh token expired): the user is redirected to `/login` with a notification ("Your session expired. Please log in again.") - [ ] The original user action is not lost if possible (e.g. form data is preserved) - [ ] Only one refresh attempt is made per 401 — infinite retry loops are prevented - [ ] **Security**: The token refresh logic is centralised in the API client, not duplicated across components --- ## Epic: System Initialization ### US-06: Initialize system as first Owner **Persona**: Anonymous Visitor As the first visitor to a new CMS installation, I want to create an Owner account during system setup, so that the platform is ready for use. **Acceptance Criteria**: - [ ] The `/setup` page contains a form with email and password fields - [ ] The form validates email format and password minimum length (8 characters) - [ ] On submit: `POST /setup/owner` is called with the provided credentials - [ ] On success: the user is redirected to `/login` with a success toast ("System initialized. Please log in.") - [ ] On failure (e.g. system already initialized): an appropriate error message is shown and the user is redirected to `/login` - [ ] **Edge case**: If the user navigates to `/setup` when the system is already initialized, they are redirected to `/login` - [ ] **Security**: The setup page is only reachable when `GET /setup/status` returns `{ initialized: false }` --- ### US-07: Redirect to setup when system is not initialized **Persona**: Anonymous Visitor As a visitor to an uninitialized CMS, I want to be automatically redirected to the setup page, so that I know the system needs configuration before use. **Acceptance Criteria**: - [ ] On app startup, `GET /setup/status` is called before rendering any page - [ ] If `initialized: false`: all routes redirect to `/setup` - [ ] If `initialized: true`: normal routing applies (login, protected routes, etc.) - [ ] During the status check, a loading state is shown - [ ] **Edge case**: If the `/setup/status` call fails, an error page is shown with a retry option - [ ] **Security**: The `/setup/owner` endpoint is disabled server-side once initialized; the frontend check is defence-in-depth only --- ## Epic: Dashboard ### US-08: View dashboard after login **Persona**: Owner, Admin, User As an authenticated user, I want to see a dashboard after logging in, so that I get an overview of the system's state and quick access to key functions. **Acceptance Criteria**: - [ ] The `/` (or `/dashboard`) route renders the dashboard page for all authenticated users - [ ] The dashboard displays a welcome message with the user's name and role - [ ] The dashboard displays at least one summary widget (e.g. system status indicator) - [ ] The layout uses the sidebar navigation from the example app design - [ ] **Edge case**: If user data fails to load, a graceful error state is shown --- ### US-09: View system availability status on dashboard **Persona**: Owner, Admin, User As an authenticated user, I want to see the current system availability status on the dashboard, so that I am immediately aware of any maintenance or outage. **Acceptance Criteria**: - [ ] The dashboard calls `GET /availability/status` on load - [ ] The status is displayed with a colour-coded indicator: Available (green), Maintenance (yellow), Unavailable (red) - [ ] The status message/reason is shown alongside the indicator if present - [ ] If the API call fails: the indicator shows "Unknown" with a retry option - [ ] The status is refreshed when the user navigates back to the dashboard --- ## Epic: User Management ### US-10: View list of users **Persona**: Owner, Admin As an Owner or Admin, I want to see a list of all platform users, so that I can manage team membership and access. **Acceptance Criteria**: - [ ] The `/users` page is accessible to Owner and Admin roles only - [ ] The page displays a table/list with each user's name, email, role, and active status - [ ] If the user list is empty, an empty state message is shown - [ ] If the API call fails, an error state is shown with a retry option - [ ] **Security**: Navigating to `/users` as a User role redirects to an "Access Denied" page or back to `/` --- ### US-11: Invite a new user **Persona**: Owner, Admin As an Owner or Admin, I want to invite a new user by providing their email address and selecting their role, so that they can join the platform. **Acceptance Criteria**: - [ ] An "Invite User" button is visible on the `/users` page for Owner and Admin roles - [ ] Clicking the button opens a dialog/modal with an email field and a role selector (Admin, User) - [ ] The email field validates format before submission - [ ] The role selector does NOT allow selecting Owner (Admins cannot create other Owners; Owner-role invites are also restricted unless the inviter is an Owner — consider showing Owner option only when logged in as Owner) - [ ] On submit: `POST /users/invite` is called - [ ] On success: the dialog remains open and transitions to the "share link" step (US-12) - [ ] On failure (e.g. email already registered): an appropriate inline error is shown - [ ] **Security**: The invite action is only available to Owner and Admin; role selection is validated server-side --- ### US-12: Share invite link after invitation is created **Persona**: Owner, Admin As an Owner or Admin, I want to see and copy the generated invite link after creating an invitation, so that I can share it with the invited person. **Acceptance Criteria**: - [ ] After a successful invitation (US-11), the dialog shows the generated invite link - [ ] A "Copy to clipboard" button is present; clicking it copies the link and shows a success toast - [ ] The link is displayed as readable text (not just a button) - [ ] A "Done" button closes the dialog - [ ] **Edge case**: If the clipboard API is unavailable (e.g. non-HTTPS context), a fallback allows manual selection of the text --- ### US-13: Complete account setup via invitation link **Persona**: New Invited User As a person who received an invitation link, I want to complete my account setup by setting a password, so that I can log in to the platform. **Acceptance Criteria**: - [ ] Navigating to `/invite/complete?token={token}` triggers `GET /users/validate-invitation?token={token}` - [ ] If the token is valid: a form is shown with a display name field and a password field (with confirmation) - [ ] Password must meet the backend requirements: minimum 8 characters, at least 1 uppercase letter, 1 lowercase letter, 1 digit, and 1 non-alphanumeric character — these rules are enforced client-side before submission and validated server-side - [ ] Inline validation messages indicate which specific rule is not yet met (e.g. "Must contain at least 1 uppercase letter") - [ ] Password confirmation must match; mismatch shows an inline error - [ ] On submit: `POST /users/complete-setup` is called with the token, display name, and password - [ ] On success: a confirmation message is shown ("Your account is ready. You can now log in.") with a link to `/login` - [ ] The form shows a loading state during submission - [ ] **Security**: The token is sent to the backend for server-side validation; client-side token inspection is never used for access decisions --- ### US-14: Handle expired or invalid invitation token **Persona**: New Invited User As a person with an expired or already-used invitation link, I want to see a clear error message, so that I understand why I cannot proceed and know what to do next. **Acceptance Criteria**: - [ ] If `GET /users/validate-invitation` returns an error (expired, used, or invalid token): no setup form is shown - [ ] An error message is displayed: "This invitation link is invalid or has expired. Please contact your administrator for a new invite." - [ ] A link to `/login` is provided for users who already completed setup - [ ] **Edge case**: If the token query parameter is missing from the URL, the same error state is shown - [ ] **Security**: No partial form data is shown when token validation fails --- ## Epic: Profile ### US-15: View own profile information **Persona**: Owner, Admin, User As an authenticated user, I want to see my profile information, so that I can verify my account details. **Acceptance Criteria**: - [ ] The `/profile` page is accessible to all authenticated users - [ ] The page displays the user's display name, email address, and role - [ ] All fields are read-only in v1 - [ ] A placeholder/note indicates that profile editing will be available in a future version - [ ] **Edge case**: If the user data cannot be loaded, an error state is shown --- ## Epic: System Settings ### US-16: View system availability status in settings **Persona**: Owner As an Owner, I want to view the current system availability status in the System Settings page, so that I have a central place for system-level information. **Acceptance Criteria**: - [ ] The `/settings` page is accessible to Owner only - [ ] The page displays the current system status (Available / Maintenance / Unavailable) and reason from `GET /availability/status` - [ ] The initialized status of the system is also shown (always "Initialized" when this page is reachable) - [ ] Placeholder sections for future settings categories are visible (e.g. "Module Settings", "CMS Configuration") - [ ] **Edge case**: If the availability API call fails, the status shows "Unknown" with a retry option --- ### US-17: Access denied for System Settings for non-Owners **Persona**: Admin, User As an Admin or User, I want to be prevented from accessing the System Settings page, so that system-level configuration is protected. **Acceptance Criteria**: - [ ] Navigating to `/settings` as an Admin or User redirects to an "Access Denied" page or to `/` - [ ] The System Settings item is NOT shown in the sidebar for Admin or User roles - [ ] **Security**: The redirect is enforced client-side as defence-in-depth; the backend also enforces authorisation --- ## Epic: Navigation & Layout ### US-18: See role-appropriate navigation items in sidebar **Persona**: Owner, Admin, User As an authenticated user, I want to see only the navigation items relevant to my role in the sidebar, so that the interface is uncluttered and I am not confused by inaccessible sections. **Acceptance Criteria**: - [ ] Dashboard: visible to all authenticated users - [ ] User Management: visible to Owner and Admin only - [ ] System Settings: visible to Owner only - [ ] CMS Management: visible to Owner only (feature is for managing other client CMS instances; out of scope for v1 but access is restricted from the start) - [ ] Profile: visible to all authenticated users - [ ] Logout: visible to all authenticated users - [ ] The sidebar is responsive: collapses to an icon-only view or a hamburger menu on small screens - [ ] The currently active route is highlighted in the sidebar --- ### US-19: Toggle dark/light theme **Persona**: Owner, Admin, User As an authenticated user, I want to switch between dark and light themes, so that I can use the interface comfortably in different lighting conditions. **Acceptance Criteria**: - [ ] A theme toggle button is present in the UI (sidebar footer or top bar) - [ ] Clicking the toggle switches between light and dark mode immediately - [ ] The chosen theme is persisted in `localStorage` under a well-named key (e.g. `cms-theme`) - [ ] On app load, the persisted theme preference is applied before the first render (no flash) - [ ] If no preference is stored, the system (OS) preference is used as default - [ ] **Security**: Only theme preference is stored in localStorage — no auth data --- ## Epic: CMS Management (Placeholder) ### US-20: View CMS management placeholder page **Persona**: Owner As an Owner, I want to navigate to the CMS Management section, so that I know where multi-CMS management features will be available in the future. **Acceptance Criteria**: - [ ] The `/cms` route renders a CMS Management page for Owner only - [ ] Navigating to `/cms` as Admin or User redirects to an "Access Denied" page or back to `/` - [ ] The CMS Management item is NOT shown in the sidebar for Admin or User roles - [ ] The page displays a clear "Work in Progress" or "Coming Soon" message - [ ] A brief description explains that this section will allow managing multiple client CMS instances - [ ] The page uses the same layout as other pages (sidebar, header) - [ ] **Security**: The access restriction is enforced client-side as defence-in-depth; the backend also enforces authorisation --- ## Future Features (Brief Notes — Out of Scope for v1) - **Profile editing** (`/profile`): Change display name and password — backend endpoints not yet confirmed - **Availability status management** (`/settings`): Allow Owner to set status to Available/Maintenance/Unavailable — requires CMS availability master module - **User deactivation/editing** (`/users`): Deactivate or edit existing users — backend endpoints not yet confirmed - **CMS content modules**: Actual content management pages — dependent on backend CMS modules being built