feat: remove hashed passwords feature spec and migrate to archive
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m6s

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
This commit is contained in:
2026-02-20 16:31:13 -05:00
parent ba909100a7
commit 55e7dc7568
15 changed files with 657 additions and 38 deletions

View File

@@ -0,0 +1,141 @@
# 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):
```json
{ "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
- [x] 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
- [x] `auth.ts` — non-persistent: `authenticateParent(false)` sets expiry to `now + 60s`; `isParentAuthenticated` returns `false` after watcher fires past expiry (via fake timers).
- [x] `auth.ts` — persistent: `authenticateParent(true)` sets `parentAuthExpiresAt` to `now + 2 days`; `isParentAuthenticated` returns `false` after watcher fires past 2-day expiry.
- [x] `auth.ts``logoutParent()` clears refs, stops watcher.
- [x] `auth.ts``loginUser()` calls `logoutParent()` clearing all parent auth state.
- [x] `LoginButton.vue` — checkbox is unchecked by default; checking it and submitting calls `authenticateParent(true)`.
- [x] `LoginButton.vue` — submitting without checkbox calls `authenticateParent(false)`.
- [x] `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
- [x] No backend changes required; all work is frontend-only.
### Frontend
- [x] PIN modal includes an unchecked "Stay in parent mode on this device" checkbox.
- [x] Non-persistent mode: parent auth is memory-only, expires after 1 minute, and is lost on page reload.
- [x] Persistent mode: `localStorage['parentAuth']` is written with a 2-day `expiresAt` timestamp; auth survives page reload and new tabs.
- [x] Router guard redirects silently to `/child` if parent mode has expired when navigating to any `/parent` route.
- [x] Background 15-second interval also enforces expiry while the user is mid-page on a `/parent` route.
- [x] "Child Mode" button clears both persistent and non-persistent auth state completely.
- [x] A `🔒` emoji badge appears on the lower-left of the parent avatar button only when persistent mode is active.
- [x] Opening a new tab while in persistent mode correctly restores parent mode from `localStorage`.
- [x] All frontend tests listed above pass.

49
.github/specs/template/feat-template.md vendored Normal file
View File

@@ -0,0 +1,49 @@
# Feature:
## Overview
**Goal:**
**User Story:**
**Rules:**
---
## Data Model Changes
### Backend Model
### Frontend Model
---
## Backend Implementation
## Backend Tests
- [ ]
---
## Frontend Implementation
## Frontend Tests
- [ ]
---
## Future Considerations
---
## Acceptance Criteria (Definition of Done)
### Backend
- [ ]
### Frontend
- [ ]