fix: update login token expiration to 62 days chore: bump version to 1.0.5RC1 test: add isParentPersistent to LoginButton.spec.ts refactor: rename Assign Tasks button to Assign Chores in ParentView.vue refactor: rename Assign Tasks to Assign Chores in TaskAssignView.vue feat: add stay in parent mode checkbox and badge in LoginButton.vue test: enhance LoginButton.spec.ts with persistent mode tests test: add authGuard.spec.ts for logoutParent and enforceParentExpiry feat: implement parent mode expiry logic in auth.ts test: add auth.expiry.spec.ts for parent mode expiry tests chore: create template for feature specs
7.6 KiB
Feature: Persistent and non-persistent parent mode
Overview
When a parent is prompted to input the parent PIN, a checkbox should also be available that asks if the parent wants to 'stay' in parent mode. If that is checked, the parent mode remains persistent on the device until child mode is entered or until an expiry time of 2 days. When the checkbox is not enabled (default) the parent authentication should expire in 1 minute or the next reload of the site.
Goal: A parent that has a dedicated device should stay in parent mode for a max of 2 days before having to re-enter the PIN, a device dedicated to the child should not stay in parent mode for more than a minute before reverting back to child mode.
User Story: As a parent, I want my personal device to be able to stay in parent mode until I enter child mode or 2 days expire. As a parent, on my child's device, I want to be able to enter parent mode to make a change or two and not have to worry about exiting parent mode.
Rules: Use .github/copilot-instructions.md
Common files: frontend\vue-app\src\components\shared\LoginButton.vue frontend\vue-app\src\stores\auth.ts frontend\vue-app\src\router\index.ts
Data Model Changes
Backend Model
No backend changes required. PIN validation is already handled server-side via POST /user/check-pin. Parent mode session duration is a purely client-side concern.
Frontend Model
localStorage['parentAuth'] (written only for persistent mode):
{ "expiresAt": 1234567890123 }
- Present only when "Stay in parent mode" was checked at PIN entry.
- Removed when the user clicks "Child Mode", on explicit logout, or when found expired on store init.
Auth store state additions (frontend/vue-app/src/stores/auth.ts):
parentAuthExpiresAt: Ref<number | null>— epoch ms timestamp;nullwhen not authenticated. Memory-only for non-persistent sessions, restored fromlocalStoragefor persistent ones.isParentPersistent: Ref<boolean>—truewhen the current parent session was marked "stay".isParentAuthenticated: Ref<boolean>— plain ref set totruebyauthenticateParent()andfalsebylogoutParent(). Expiry is enforced by the 15-second background watcher and the router guard callingenforceParentExpiry().
Backend Implementation
No backend changes required.
Backend Tests
- No new backend tests required.
Frontend Implementation
1. Refactor auth.ts — expiry-aware state
- Remove the plain
ref<boolean>isParentAuthenticatedand thewatchthat wrote'true'/'false'tolocalStorage['isParentAuthenticated']. - Add
parentAuthExpiresAt: ref<number | null>(initialized tonull). - Add
isParentPersistent: ref<boolean>(initialized tofalse). - Keep
isParentAuthenticatedas a plainref<boolean>— set explicitly byauthenticateParent()andlogoutParent(). A background watcher and router guard enforce expiry by callinglogoutParent()whenDate.now() >= parentAuthExpiresAt.value. - Update
authenticateParent(persistent: boolean):- Non-persistent: set
parentAuthExpiresAt.value = Date.now() + 60_000,isParentPersistent.value = false. Write nothing tolocalStorage. State is lost on page reload naturally. - Persistent: set
parentAuthExpiresAt.value = Date.now() + 172_800_000(2 days),isParentPersistent.value = true. Write{ expiresAt }tolocalStorage['parentAuth']. - Both: set
isParentAuthenticated.value = true, callstartParentExpiryWatcher().
- Non-persistent: set
- Update
logoutParent(): clear all three refs (null/false/false), removelocalStorage['parentAuth'], callstopParentExpiryWatcher(). - Update
loginUser(): calllogoutParent()internally (already resets parent state on fresh login). - On store initialization: read
localStorage['parentAuth']; if present andexpiresAt > Date.now(), restore as persistent auth; otherwise remove the stale key.
2. Add background expiry watcher to auth.ts
- Export
startParentExpiryWatcher()andstopParentExpiryWatcher()that manage a 15-secondsetInterval. - The interval checks
Date.now() >= parentAuthExpiresAt.value; if true, callslogoutParent()and navigates to/childviawindow.location.href. This enforces expiry even while a parent is mid-page on a/parentroute.
3. Update router navigation guard — router/index.ts
- Import
logoutParentandenforceParentExpiryfrom the auth store. - Before checking parent route access, call
enforceParentExpiry()which evaluatesDate.now() >= parentAuthExpiresAt.valuedirectly and callslogoutParent()if expired. - If not authenticated after the check: call
logoutParent()(cleanup) then redirect to/child.
4. Update PIN modal in LoginButton.vue — checkbox
- Add
stayInParentMode: ref<boolean>(defaultfalse). - Add a checkbox below the PIN input, labelled "Stay in parent mode on this device".
- Style checkbox with
:rootCSS variables fromcolors.css. - Update
submit()to callauthenticateParent(stayInParentMode.value). - Reset
stayInParentMode.value = falsewhen the modal closes.
5. Add lock badge to avatar button — LoginButton.vue
- Import
isParentPersistentfrom the auth store. - Wrap the existing avatar button in a
position: relativecontainer. - When
isParentAuthenticated && isParentPersistent, render a small🔒emoji element absolutely positioned atbottom: -2px; left: -2pxwith a font size of ~10px. - This badge disappears automatically when "Child Mode" is clicked (clears
isParentPersistent).
Frontend Tests
auth.ts— non-persistent:authenticateParent(false)sets expiry tonow + 60s;isParentAuthenticatedreturnsfalseafter watcher fires past expiry (via fake timers).auth.ts— persistent:authenticateParent(true)setsparentAuthExpiresAttonow + 2 days;isParentAuthenticatedreturnsfalseafter watcher fires past 2-day expiry.auth.ts—logoutParent()clears refs, stops watcher.auth.ts—loginUser()callslogoutParent()clearing all parent auth state.LoginButton.vue— checkbox is unchecked by default; checking it and submitting callsauthenticateParent(true).LoginButton.vue— submitting without checkbox callsauthenticateParent(false).LoginButton.vue— lock badge🔒is visible only whenisParentAuthenticated && isParentPersistent.
Future Considerations
- Could offer a configurable expiry duration (e.g. 1 day, 3 days, 7 days) rather than a fixed 2-day cap.
- Could show a "session expiring soon" warning for the persistent mode (e.g. banner appears 1 hour before the 2-day expiry).
Acceptance Criteria (Definition of Done)
Backend
- No backend changes required; all work is frontend-only.
Frontend
- PIN modal includes an unchecked "Stay in parent mode on this device" checkbox.
- Non-persistent mode: parent auth is memory-only, expires after 1 minute, and is lost on page reload.
- Persistent mode:
localStorage['parentAuth']is written with a 2-dayexpiresAttimestamp; auth survives page reload and new tabs. - Router guard redirects silently to
/childif parent mode has expired when navigating to any/parentroute. - Background 15-second interval also enforces expiry while the user is mid-page on a
/parentroute. - "Child Mode" button clears both persistent and non-persistent auth state completely.
- A
🔒emoji badge appears on the lower-left of the parent avatar button only when persistent mode is active. - Opening a new tab while in persistent mode correctly restores parent mode from
localStorage. - All frontend tests listed above pass.