Files
chore/.github/specs/archive/feat-parent-mode-expire.md
Ryan Kegel 55e7dc7568
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m6s
feat: remove hashed passwords feature spec and migrate to archive
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
2026-02-20 16:31:13 -05:00

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; null when not authenticated. Memory-only for non-persistent sessions, restored from localStorage for persistent ones.
  • isParentPersistent: Ref<boolean>true when the current parent session was marked "stay".
  • isParentAuthenticated: Ref<boolean> — plain ref set to true by authenticateParent() and false by logoutParent(). Expiry is enforced by the 15-second background watcher and the router guard calling enforceParentExpiry().

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> isParentAuthenticated and the watch that wrote 'true'/'false' to localStorage['isParentAuthenticated'].
  • Add parentAuthExpiresAt: ref<number | null> (initialized to null).
  • Add isParentPersistent: ref<boolean> (initialized to false).
  • Keep isParentAuthenticated as a plain ref<boolean> — set explicitly by authenticateParent() and logoutParent(). A background watcher and router guard enforce expiry by calling logoutParent() when Date.now() >= parentAuthExpiresAt.value.
  • Update authenticateParent(persistent: boolean):
    • Non-persistent: set parentAuthExpiresAt.value = Date.now() + 60_000, isParentPersistent.value = false. Write nothing to localStorage. State is lost on page reload naturally.
    • Persistent: set parentAuthExpiresAt.value = Date.now() + 172_800_000 (2 days), isParentPersistent.value = true. Write { expiresAt } to localStorage['parentAuth'].
    • Both: set isParentAuthenticated.value = true, call startParentExpiryWatcher().
  • Update logoutParent(): clear all three refs (null/false/false), remove localStorage['parentAuth'], call stopParentExpiryWatcher().
  • Update loginUser(): call logoutParent() internally (already resets parent state on fresh login).
  • On store initialization: read localStorage['parentAuth']; if present and expiresAt > Date.now(), restore as persistent auth; otherwise remove the stale key.

2. Add background expiry watcher to auth.ts

  • Export startParentExpiryWatcher() and stopParentExpiryWatcher() that manage a 15-second setInterval.
  • The interval checks Date.now() >= parentAuthExpiresAt.value; if true, calls logoutParent() and navigates to /child via window.location.href. This enforces expiry even while a parent is mid-page on a /parent route.

3. Update router navigation guard — router/index.ts

  • Import logoutParent and enforceParentExpiry from the auth store.
  • Before checking parent route access, call enforceParentExpiry() which evaluates Date.now() >= parentAuthExpiresAt.value directly and calls logoutParent() 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> (default false).
  • Add a checkbox below the PIN input, labelled "Stay in parent mode on this device".
  • Style checkbox with :root CSS variables from colors.css.
  • Update submit() to call authenticateParent(stayInParentMode.value).
  • Reset stayInParentMode.value = false when the modal closes.

5. Add lock badge to avatar button — LoginButton.vue

  • Import isParentPersistent from the auth store.
  • Wrap the existing avatar button in a position: relative container.
  • When isParentAuthenticated && isParentPersistent, render a small 🔒 emoji element absolutely positioned at bottom: -2px; left: -2px with 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 to now + 60s; isParentAuthenticated returns false after watcher fires past expiry (via fake timers).
  • auth.ts — persistent: authenticateParent(true) sets parentAuthExpiresAt to now + 2 days; isParentAuthenticated returns false after watcher fires past 2-day expiry.
  • auth.tslogoutParent() clears refs, stops watcher.
  • auth.tsloginUser() calls logoutParent() clearing all parent auth state.
  • LoginButton.vue — checkbox is unchecked by default; checking it and submitting calls authenticateParent(true).
  • LoginButton.vue — submitting without checkbox calls authenticateParent(false).
  • LoginButton.vue — lock badge 🔒 is visible only when isParentAuthenticated && 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-day expiresAt timestamp; auth survives page reload and new tabs.
  • Router guard redirects silently to /child if parent mode has expired when navigating to any /parent route.
  • Background 15-second interval also enforces expiry while the user is mid-page on a /parent route.
  • "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.